@redocly/openapi-core 1.23.1 → 1.25.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/lib/config/all.js +3 -0
  3. package/lib/config/config-resolvers.d.ts +1 -1
  4. package/lib/config/config-resolvers.js +42 -26
  5. package/lib/config/config.js +4 -1
  6. package/lib/config/minimal.js +3 -0
  7. package/lib/config/recommended-strict.js +3 -0
  8. package/lib/config/recommended.js +3 -0
  9. package/lib/config/types.d.ts +10 -0
  10. package/lib/rules/arazzo/criteria-unique.d.ts +2 -0
  11. package/lib/rules/arazzo/criteria-unique.js +65 -0
  12. package/lib/rules/arazzo/index.js +6 -0
  13. package/lib/rules/spot/no-actions-type-end.d.ts +2 -0
  14. package/lib/rules/spot/no-actions-type-end.js +28 -0
  15. package/lib/rules/spot/no-criteria-xpath.d.ts +2 -0
  16. package/lib/rules/spot/no-criteria-xpath.js +21 -0
  17. package/lib/types/arazzo.js +1 -1
  18. package/lib/types/redocly-yaml.d.ts +1 -1
  19. package/lib/types/redocly-yaml.js +3 -0
  20. package/package.json +2 -2
  21. package/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +6 -0
  22. package/src/config/__tests__/config-resolvers.test.ts +54 -0
  23. package/src/config/__tests__/fixtures/resolve-config/local-config-with-plugin-init.yaml +2 -0
  24. package/src/config/__tests__/fixtures/resolve-config/local-config-with-realm-plugin.yaml +2 -0
  25. package/src/config/__tests__/fixtures/resolve-config/plugin-with-init-logic.js +9 -0
  26. package/src/config/__tests__/fixtures/resolve-config/realm-plugin.js +12 -0
  27. package/src/config/all.ts +3 -0
  28. package/src/config/config-resolvers.ts +52 -29
  29. package/src/config/config.ts +4 -1
  30. package/src/config/minimal.ts +3 -0
  31. package/src/config/recommended-strict.ts +3 -0
  32. package/src/config/recommended.ts +3 -0
  33. package/src/config/types.ts +16 -0
  34. package/src/rules/arazzo/__tests__/criteria-unique.test.ts +161 -0
  35. package/src/rules/arazzo/__tests__/no-actions-type-end.test.ts +122 -0
  36. package/src/rules/arazzo/__tests__/no-criteria-xpath.test.ts +127 -0
  37. package/src/rules/arazzo/criteria-unique.ts +63 -0
  38. package/src/rules/arazzo/index.ts +6 -0
  39. package/src/rules/spot/no-actions-type-end.ts +27 -0
  40. package/src/rules/spot/no-criteria-xpath.ts +20 -0
  41. package/src/types/arazzo.ts +1 -1
  42. package/src/types/redocly-yaml.ts +3 -0
  43. package/tsconfig.tsbuildinfo +1 -1
@@ -43,6 +43,9 @@ const DEFAULT_PROJECT_PLUGIN_PATHS = ['@theme/plugin.js', '@theme/plugin.cjs', '
43
43
  // Workaround for dynamic imports being transpiled to require by Typescript: https://github.com/microsoft/TypeScript/issues/43329#issuecomment-811606238
44
44
  const _importDynamic = new Function('modulePath', 'return import(modulePath)');
45
45
 
46
+ // Cache instantiated plugins during a single execution
47
+ const pluginsCache: Map<string, Plugin> = new Map();
48
+
46
49
  export async function resolveConfigFileAndRefs({
47
50
  configPath,
48
51
  externalRefResolver = new BaseResolver(),
@@ -111,9 +114,9 @@ export async function resolveConfig({
111
114
  );
112
115
  }
113
116
 
114
- function getDefaultPluginPath(configPath: string): string | undefined {
117
+ function getDefaultPluginPath(configDir: string): string | undefined {
115
118
  for (const pluginPath of DEFAULT_PROJECT_PLUGIN_PATHS) {
116
- const absolutePluginPath = path.resolve(path.dirname(configPath), pluginPath);
119
+ const absolutePluginPath = path.resolve(configDir, pluginPath);
117
120
  if (existsSync(absolutePluginPath)) {
118
121
  return pluginPath;
119
122
  }
@@ -123,32 +126,58 @@ function getDefaultPluginPath(configPath: string): string | undefined {
123
126
 
124
127
  export async function resolvePlugins(
125
128
  plugins: (string | Plugin)[] | null,
126
- configPath: string = ''
129
+ configDir: string = ''
127
130
  ): Promise<Plugin[]> {
128
131
  if (!plugins) return [];
129
132
 
130
133
  // TODO: implement or reuse Resolver approach so it will work in node and browser envs
131
- const requireFunc = async (plugin: string | Plugin): Promise<ImportedPlugin | undefined> => {
134
+ const requireFunc = async (plugin: string | Plugin): Promise<Plugin | undefined> => {
132
135
  if (isString(plugin)) {
133
136
  try {
134
- const maybeAbsolutePluginPath = path.resolve(path.dirname(configPath), plugin);
137
+ const maybeAbsolutePluginPath = path.resolve(configDir, plugin);
135
138
 
136
139
  const absolutePluginPath = existsSync(maybeAbsolutePluginPath)
137
140
  ? maybeAbsolutePluginPath
138
141
  : // For plugins imported from packages specifically
139
142
  require.resolve(plugin);
140
143
 
141
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
142
- // @ts-ignore
143
- if (typeof __webpack_require__ === 'function') {
144
+ if (!pluginsCache.has(absolutePluginPath)) {
145
+ let requiredPlugin: ImportedPlugin | undefined;
146
+
144
147
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
145
148
  // @ts-ignore
146
- return __non_webpack_require__(absolutePluginPath);
147
- } else {
148
- // you can import both cjs and mjs
149
- const mod = await _importDynamic(pathToFileURL(absolutePluginPath).href);
150
- return mod.default || mod;
149
+ if (typeof __webpack_require__ === 'function') {
150
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
151
+ // @ts-ignore
152
+ requiredPlugin = __non_webpack_require__(absolutePluginPath);
153
+ } else {
154
+ // you can import both cjs and mjs
155
+ const mod = await _importDynamic(pathToFileURL(absolutePluginPath).href);
156
+ requiredPlugin = mod.default || mod;
157
+ }
158
+
159
+ const pluginCreatorOptions = { contentDir: configDir };
160
+
161
+ const pluginModule = isDeprecatedPluginFormat(requiredPlugin)
162
+ ? requiredPlugin
163
+ : isCommonJsPlugin(requiredPlugin)
164
+ ? await requiredPlugin(pluginCreatorOptions)
165
+ : await requiredPlugin?.default?.(pluginCreatorOptions);
166
+
167
+ if (pluginModule?.id && isDeprecatedPluginFormat(requiredPlugin)) {
168
+ logger.info(`Deprecated plugin format detected: ${pluginModule.id}\n`);
169
+ }
170
+
171
+ if (pluginModule) {
172
+ pluginsCache.set(absolutePluginPath, {
173
+ ...pluginModule,
174
+ path: plugin,
175
+ absolutePath: absolutePluginPath,
176
+ });
177
+ }
151
178
  }
179
+
180
+ return pluginsCache.get(absolutePluginPath);
152
181
  } catch (e) {
153
182
  throw new Error(`Failed to load plugin "${plugin}": ${e.message}\n\n${e.stack}`);
154
183
  }
@@ -162,7 +191,7 @@ export async function resolvePlugins(
162
191
  /**
163
192
  * Include the default plugin automatically if it's not in configuration
164
193
  */
165
- const defaultPluginPath = getDefaultPluginPath(configPath);
194
+ const defaultPluginPath = getDefaultPluginPath(configDir);
166
195
  if (defaultPluginPath) {
167
196
  plugins.push(defaultPluginPath);
168
197
  }
@@ -182,24 +211,12 @@ export async function resolvePlugins(
182
211
  resolvedPlugins.add(p);
183
212
  }
184
213
 
185
- const requiredPlugin: ImportedPlugin | undefined = await requireFunc(p);
186
-
187
- const pluginCreatorOptions = { contentDir: path.dirname(configPath) };
188
-
189
- const pluginModule = isDeprecatedPluginFormat(requiredPlugin)
190
- ? requiredPlugin
191
- : isCommonJsPlugin(requiredPlugin)
192
- ? await requiredPlugin(pluginCreatorOptions)
193
- : await requiredPlugin?.default?.(pluginCreatorOptions);
214
+ const pluginModule: Plugin | undefined = await requireFunc(p);
194
215
 
195
216
  if (!pluginModule) {
196
217
  return;
197
218
  }
198
219
 
199
- if (isString(p) && pluginModule.id && isDeprecatedPluginFormat(requiredPlugin)) {
200
- logger.info(`Deprecated plugin format detected: ${pluginModule.id}\n`);
201
- }
202
-
203
220
  const id = pluginModule.id;
204
221
  if (typeof id !== 'string') {
205
222
  throw new Error(
@@ -313,7 +330,10 @@ export async function resolvePlugins(
313
330
  plugin.assertions = pluginModule.assertions;
314
331
  }
315
332
 
316
- return plugin;
333
+ return {
334
+ ...pluginModule,
335
+ ...plugin,
336
+ };
317
337
  })
318
338
  );
319
339
 
@@ -371,7 +391,10 @@ async function resolveAndMergeNestedStyleguideConfig(
371
391
  ? // In browser, we don't support plugins from config file yet
372
392
  [defaultPlugin]
373
393
  : getUniquePlugins(
374
- await resolvePlugins([...(styleguideConfig?.plugins || []), defaultPlugin], configPath)
394
+ await resolvePlugins(
395
+ [...(styleguideConfig?.plugins || []), defaultPlugin],
396
+ path.dirname(configPath)
397
+ )
375
398
  );
376
399
  const pluginPaths = styleguideConfig?.plugins
377
400
  ?.filter(isString)
@@ -73,7 +73,10 @@ export class StyleguideConfig {
73
73
  [SpecVersion.OAS3_1]: { ...rawConfig.rules, ...rawConfig.oas3_1Rules },
74
74
  [SpecVersion.Async2]: { ...rawConfig.rules, ...rawConfig.async2Rules },
75
75
  [SpecVersion.Async3]: { ...rawConfig.rules, ...rawConfig.async3Rules },
76
- [SpecVersion.Arazzo]: { ...rawConfig.arazzoRules },
76
+ [SpecVersion.Arazzo]: {
77
+ ...rawConfig.arazzoRules,
78
+ ...(rawConfig.rules?.assertions ? { assertions: rawConfig.rules.assertions } : {}),
79
+ },
77
80
  };
78
81
 
79
82
  this.preprocessors = {
@@ -121,6 +121,9 @@ const minimal: PluginStyleguideConfig<'built-in'> = {
121
121
  'step-onSuccess-unique': 'off',
122
122
  'step-onFailure-unique': 'off',
123
123
  'requestBody-replacements-unique': 'off',
124
+ 'no-criteria-xpath': 'off',
125
+ 'no-actions-type-end': 'off',
126
+ 'criteria-unique': 'off',
124
127
  },
125
128
  };
126
129
 
@@ -121,6 +121,9 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = {
121
121
  'step-onSuccess-unique': 'error',
122
122
  'step-onFailure-unique': 'error',
123
123
  'requestBody-replacements-unique': 'error',
124
+ 'no-criteria-xpath': 'error',
125
+ 'no-actions-type-end': 'error',
126
+ 'criteria-unique': 'error',
124
127
  },
125
128
  };
126
129
 
@@ -121,6 +121,9 @@ const recommended: PluginStyleguideConfig<'built-in'> = {
121
121
  'step-onSuccess-unique': 'warn',
122
122
  'step-onFailure-unique': 'warn',
123
123
  'requestBody-replacements-unique': 'warn',
124
+ 'no-criteria-xpath': 'warn',
125
+ 'no-actions-type-end': 'warn',
126
+ 'criteria-unique': 'warn',
124
127
  },
125
128
  };
126
129
 
@@ -30,6 +30,7 @@ import type {
30
30
  BuiltInOAS3RuleId,
31
31
  BuiltInArazzoRuleId,
32
32
  } from '../types/redocly-yaml';
33
+ import type { JSONSchema } from 'json-schema-to-ts';
33
34
 
34
35
  export type RuleSeverity = ProblemSeverity | 'off';
35
36
 
@@ -136,12 +137,27 @@ export type AssertionsConfig = Record<string, CustomFunction>;
136
137
 
137
138
  export type Plugin = {
138
139
  id: string;
140
+
139
141
  configs?: Record<string, PluginStyleguideConfig>;
140
142
  rules?: CustomRulesConfig;
141
143
  preprocessors?: PreprocessorsConfig;
142
144
  decorators?: DecoratorsConfig;
143
145
  typeExtension?: TypeExtensionsConfig;
144
146
  assertions?: AssertionsConfig;
147
+
148
+ // Realm properties
149
+ path?: string;
150
+ absolutePath?: string;
151
+ processContent?: (actions: any, content: any) => Promise<void> | void;
152
+ afterRoutesCreated?: (actions: any, content: any) => Promise<void> | void;
153
+ loaders?: Record<
154
+ string,
155
+ (path: string, context: any, reportError: (error: Error) => void) => Promise<unknown>
156
+ >;
157
+ requiredEntitlements?: string[];
158
+ ssoConfigSchema?: JSONSchema;
159
+ redoclyConfigSchema?: JSONSchema;
160
+ ejectIgnore?: string[];
145
161
  };
146
162
 
147
163
  type PluginCreatorOptions = {
@@ -0,0 +1,161 @@
1
+ import { outdent } from 'outdent';
2
+ import { lintDocument } from '../../../lint';
3
+ import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
4
+ import { BaseResolver } from '../../../resolve';
5
+
6
+ describe('Arazzo criteria-unique', () => {
7
+ const document = parseYamlToDocument(
8
+ outdent`
9
+ arazzo: '1.0.0'
10
+ info:
11
+ title: Cool API
12
+ version: 1.0.0
13
+ description: A cool API
14
+ sourceDescriptions:
15
+ - name: museum-api
16
+ type: openapi
17
+ url: openapi.yaml
18
+ workflows:
19
+ - workflowId: get-museum-hours
20
+ description: This workflow demonstrates how to get the museum opening hours and buy tickets.
21
+ parameters:
22
+ - in: header
23
+ name: Authorization
24
+ value: Basic Og==
25
+ steps:
26
+ - stepId: create-event
27
+ description: >-
28
+ Create a new special event.
29
+ operationPath: $sourceDescriptions.museum-api#/paths/~1special-events/post
30
+ requestBody:
31
+ payload:
32
+ name: 'Mermaid Treasure Identification and Analysis'
33
+ location: 'Under the seaaa 🦀 🎶 🌊.'
34
+ eventDescription: 'Join us as we review and classify a rare collection of 20 thingamabobs, gadgets, gizmos, whoosits, and whatsits, kindly donated by Ariel.'
35
+ dates:
36
+ - '2023-09-05'
37
+ - '2023-09-08'
38
+ price: 0
39
+ successCriteria:
40
+ - condition: $statusCode == 200
41
+ - condition: $statusCode == 200
42
+ - context: $response.body
43
+ condition: $.name == 'Mermaid Treasure Identification and Analysis'
44
+ type: jsonpath
45
+ - context: $response.body
46
+ condition: $.name == 'Mermaid Treasure Identification and Analysis'
47
+ type: jsonpath
48
+ onSuccess:
49
+ - name: 'onSuccessActionName'
50
+ type: 'goto'
51
+ stepId: 'buy-ticket'
52
+ criteria:
53
+ - condition: $response.body.open == true
54
+ - condition: $response.body.open == true
55
+ onFailure:
56
+ - name: 'onFailureActionName'
57
+ type: 'goto'
58
+ stepId: 'buy-ticket'
59
+ criteria:
60
+ - condition: $response.body.open == true
61
+ - condition: $response.body.open == true
62
+ outputs:
63
+ createdEventId: $response.body.eventId
64
+ name: $response.body.name
65
+ - workflowId: get-museum-hours-2
66
+ description: This workflow demonstrates how to get the museum opening hours and buy tickets.
67
+ parameters:
68
+ - in: header
69
+ name: Authorization
70
+ value: Basic Og==
71
+ steps:
72
+ - stepId: get-museum-hours
73
+ description: >-
74
+ Get museum hours by resolving request details with getMuseumHours operationId from openapi.yaml description.
75
+ operationId: museum-api.getMuseumHours
76
+ successCriteria:
77
+ - condition: $statusCode == 200
78
+ `,
79
+ 'arazzo.yaml'
80
+ );
81
+
82
+ it('should report when the duplicated criteria exists', async () => {
83
+ const results = await lintDocument({
84
+ externalRefResolver: new BaseResolver(),
85
+ document,
86
+ config: await makeConfig({
87
+ rules: {},
88
+ arazzoRules: { 'criteria-unique': 'error' },
89
+ }),
90
+ });
91
+
92
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
93
+ [
94
+ {
95
+ "location": [
96
+ {
97
+ "pointer": "#/workflows/0/steps/0/successCriteria/1",
98
+ "reportOnKey": false,
99
+ "source": "arazzo.yaml",
100
+ },
101
+ ],
102
+ "message": "The Step SuccessCriteria items must be unique.",
103
+ "ruleId": "criteria-unique",
104
+ "severity": "error",
105
+ "suggest": [],
106
+ },
107
+ {
108
+ "location": [
109
+ {
110
+ "pointer": "#/workflows/0/steps/0/successCriteria/3",
111
+ "reportOnKey": false,
112
+ "source": "arazzo.yaml",
113
+ },
114
+ ],
115
+ "message": "The Step SuccessCriteria items must be unique.",
116
+ "ruleId": "criteria-unique",
117
+ "severity": "error",
118
+ "suggest": [],
119
+ },
120
+ {
121
+ "location": [
122
+ {
123
+ "pointer": "#/workflows/0/steps/0/onSuccess/0/criteria/1",
124
+ "reportOnKey": false,
125
+ "source": "arazzo.yaml",
126
+ },
127
+ ],
128
+ "message": "The SuccessAction criteria items must be unique.",
129
+ "ruleId": "criteria-unique",
130
+ "severity": "error",
131
+ "suggest": [],
132
+ },
133
+ {
134
+ "location": [
135
+ {
136
+ "pointer": "#/workflows/0/steps/0/onFailure/0/criteria/1",
137
+ "reportOnKey": false,
138
+ "source": "arazzo.yaml",
139
+ },
140
+ ],
141
+ "message": "The FailureAction criteria items must be unique.",
142
+ "ruleId": "criteria-unique",
143
+ "severity": "error",
144
+ "suggest": [],
145
+ },
146
+ ]
147
+ `);
148
+ });
149
+
150
+ it('should not report when the duplicated criteria exists', async () => {
151
+ const results = await lintDocument({
152
+ externalRefResolver: new BaseResolver(),
153
+ document,
154
+ config: await makeConfig({
155
+ rules: {},
156
+ }),
157
+ });
158
+
159
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
160
+ });
161
+ });
@@ -0,0 +1,122 @@
1
+ import { outdent } from 'outdent';
2
+ import { lintDocument } from '../../../lint';
3
+ import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
4
+ import { BaseResolver } from '../../../resolve';
5
+
6
+ describe('Arazzo no-actions-type-end', () => {
7
+ const document = parseYamlToDocument(
8
+ outdent`
9
+ arazzo: '1.0.0'
10
+ info:
11
+ title: Cool API
12
+ version: 1.0.0
13
+ description: A cool API
14
+ sourceDescriptions:
15
+ - name: museum-api
16
+ type: openapi
17
+ url: openapi.yaml
18
+ workflows:
19
+ - workflowId: get-museum-hours
20
+ description: This workflow demonstrates how to get the museum opening hours and buy tickets.
21
+ parameters:
22
+ - in: header
23
+ name: Authorization
24
+ value: Basic Og==
25
+ steps:
26
+ - stepId: create-event
27
+ description: >-
28
+ Create a new special event.
29
+ operationPath: $sourceDescriptions.museum-api#/paths/~1special-events/post
30
+ requestBody:
31
+ payload:
32
+ name: 'Mermaid Treasure Identification and Analysis'
33
+ location: 'Under the seaaa 🦀 🎶 🌊.'
34
+ eventDescription: 'Join us as we review and classify a rare collection of 20 thingamabobs, gadgets, gizmos, whoosits, and whatsits, kindly donated by Ariel.'
35
+ dates:
36
+ - '2023-09-05'
37
+ - '2023-09-08'
38
+ price: 0
39
+ successCriteria:
40
+ - condition: $statusCode == 201
41
+ onSuccess:
42
+ - name: 'onSuccessActionName'
43
+ type: 'end'
44
+ stepId: 'buy-ticket'
45
+ onFailure:
46
+ - name: 'onFailureActionName'
47
+ type: 'end'
48
+ stepId: 'buy-ticket'
49
+ outputs:
50
+ createdEventId: $response.body.eventId
51
+ name: $response.body.name
52
+ - workflowId: get-museum-hours-2
53
+ description: This workflow demonstrates how to get the museum opening hours and buy tickets.
54
+ parameters:
55
+ - in: header
56
+ name: Authorization
57
+ value: Basic Og==
58
+ steps:
59
+ - stepId: get-museum-hours
60
+ description: >-
61
+ Get museum hours by resolving request details with getMuseumHours operationId from openapi.yaml description.
62
+ operationId: museum-api.getMuseumHours
63
+ successCriteria:
64
+ - condition: $statusCode == 200
65
+ `,
66
+ 'arazzo.yaml'
67
+ );
68
+
69
+ it('should report when the type `end` action exists', async () => {
70
+ const results = await lintDocument({
71
+ externalRefResolver: new BaseResolver(),
72
+ document,
73
+ config: await makeConfig({
74
+ rules: {},
75
+ arazzoRules: { 'no-actions-type-end': 'error' },
76
+ }),
77
+ });
78
+
79
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
80
+ [
81
+ {
82
+ "location": [
83
+ {
84
+ "pointer": "#/workflows/0/steps/0/onSuccess/0/type",
85
+ "reportOnKey": false,
86
+ "source": "arazzo.yaml",
87
+ },
88
+ ],
89
+ "message": "The \`end\` type action is not supported by Spot.",
90
+ "ruleId": "no-actions-type-end",
91
+ "severity": "error",
92
+ "suggest": [],
93
+ },
94
+ {
95
+ "location": [
96
+ {
97
+ "pointer": "#/workflows/0/steps/0/onFailure/0/type",
98
+ "reportOnKey": false,
99
+ "source": "arazzo.yaml",
100
+ },
101
+ ],
102
+ "message": "The \`end\` type action is not supported by Spot.",
103
+ "ruleId": "no-actions-type-end",
104
+ "severity": "error",
105
+ "suggest": [],
106
+ },
107
+ ]
108
+ `);
109
+ });
110
+
111
+ it('should not report when the type `end` action exists', async () => {
112
+ const results = await lintDocument({
113
+ externalRefResolver: new BaseResolver(),
114
+ document,
115
+ config: await makeConfig({
116
+ rules: {},
117
+ }),
118
+ });
119
+
120
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
121
+ });
122
+ });
@@ -0,0 +1,127 @@
1
+ import { outdent } from 'outdent';
2
+ import { lintDocument } from '../../../lint';
3
+ import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
4
+ import { BaseResolver } from '../../../resolve';
5
+
6
+ describe('Arazzo no-criteria-xpath', () => {
7
+ const document = parseYamlToDocument(
8
+ outdent`
9
+ arazzo: '1.0.0'
10
+ info:
11
+ title: Cool API
12
+ version: 1.0.0
13
+ description: A cool API
14
+ sourceDescriptions:
15
+ - name: museum-api
16
+ type: openapi
17
+ url: openapi.yaml
18
+ workflows:
19
+ - workflowId: get-museum-hours
20
+ description: This workflow demonstrates how to get the museum opening hours and buy tickets.
21
+ parameters:
22
+ - in: header
23
+ name: Authorization
24
+ value: Basic Og==
25
+ steps:
26
+ - stepId: create-event
27
+ description: >-
28
+ Create a new special event.
29
+ operationPath: $sourceDescriptions.museum-api#/paths/~1special-events/post
30
+ requestBody:
31
+ payload:
32
+ name: 'Mermaid Treasure Identification and Analysis'
33
+ location: 'Under the seaaa 🦀 🎶 🌊.'
34
+ eventDescription: 'Join us as we review and classify a rare collection of 20 thingamabobs, gadgets, gizmos, whoosits, and whatsits, kindly donated by Ariel.'
35
+ dates:
36
+ - '2023-09-05'
37
+ - '2023-09-08'
38
+ price: 0
39
+ successCriteria:
40
+ - condition: $statusCode == 201
41
+ - context: $response.body
42
+ condition: $.name == 'Mermaid Treasure Identification and Analysis'
43
+ type:
44
+ type: jsonpath
45
+ version: draft-goessner-dispatch-jsonpath-00
46
+ - context: $response.body
47
+ condition: $.name == 'Orca Identification and Analysis'
48
+ type: xpath
49
+ - context: $response.body
50
+ condition: $.name == 'Mermaid Treasure Identification and Analysis'
51
+ type:
52
+ type: xpath
53
+ version: xpath-30
54
+ outputs:
55
+ createdEventId: $response.body.eventId
56
+ name: $response.body.name
57
+ - workflowId: get-museum-hours-2
58
+ description: This workflow demonstrates how to get the museum opening hours and buy tickets.
59
+ parameters:
60
+ - in: header
61
+ name: Authorization
62
+ value: Basic Og==
63
+ steps:
64
+ - stepId: get-museum-hours
65
+ description: >-
66
+ Get museum hours by resolving request details with getMuseumHours operationId from openapi.yaml description.
67
+ operationId: museum-api.getMuseumHours
68
+ successCriteria:
69
+ - condition: $statusCode == 200
70
+ `,
71
+ 'arazzo.yaml'
72
+ );
73
+
74
+ it('should report when the `xpath` criteria exists', async () => {
75
+ const results = await lintDocument({
76
+ externalRefResolver: new BaseResolver(),
77
+ document,
78
+ config: await makeConfig({
79
+ rules: {},
80
+ arazzoRules: { 'no-criteria-xpath': 'error' },
81
+ }),
82
+ });
83
+
84
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
85
+ [
86
+ {
87
+ "location": [
88
+ {
89
+ "pointer": "#/workflows/0/steps/0/successCriteria/2/type",
90
+ "reportOnKey": false,
91
+ "source": "arazzo.yaml",
92
+ },
93
+ ],
94
+ "message": "The \`xpath\` type criteria is not supported by Spot.",
95
+ "ruleId": "no-criteria-xpath",
96
+ "severity": "error",
97
+ "suggest": [],
98
+ },
99
+ {
100
+ "location": [
101
+ {
102
+ "pointer": "#/workflows/0/steps/0/successCriteria/3/type",
103
+ "reportOnKey": false,
104
+ "source": "arazzo.yaml",
105
+ },
106
+ ],
107
+ "message": "The \`xpath\` type criteria is not supported by Spot.",
108
+ "ruleId": "no-criteria-xpath",
109
+ "severity": "error",
110
+ "suggest": [],
111
+ },
112
+ ]
113
+ `);
114
+ });
115
+
116
+ it('should not report when the `xpath` criteria exists', async () => {
117
+ const results = await lintDocument({
118
+ externalRefResolver: new BaseResolver(),
119
+ document,
120
+ config: await makeConfig({
121
+ rules: {},
122
+ }),
123
+ });
124
+
125
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
126
+ });
127
+ });