@usearazzo/runner 1.0.1-alpha.22 → 1.0.1-alpha.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usearazzo/runner",
3
- "version": "1.0.1-alpha.22",
3
+ "version": "1.0.1-alpha.23",
4
4
  "description": "Runner for Arazzo Workflows.",
5
5
  "keywords": [
6
6
  "arazzo",
@@ -73,8 +73,8 @@
73
73
  "@swaggerexpert/json-pointer": "^3.0.2",
74
74
  "@swaggerexpert/jsonpath": "^4.0.5",
75
75
  "@swaggerexpert/media-types": "^1.0.2",
76
- "@usearazzo/parser": "1.0.1-alpha.22",
77
- "@usearazzo/resolver": "1.0.1-alpha.22",
76
+ "@usearazzo/parser": "1.0.1-alpha.23",
77
+ "@usearazzo/resolver": "1.0.1-alpha.23",
78
78
  "@xmldom/xmldom": "^0.9.12",
79
79
  "fontoxpath": "^3.34.0",
80
80
  "ramda": "^0.32.0",
@@ -94,5 +94,5 @@
94
94
  "devDependencies": {
95
95
  "swagger-client": "^3.38.4"
96
96
  },
97
- "gitHead": "e70dedbb2d249b9a6c16808048d9272609103f71"
97
+ "gitHead": "49de409b1fb851453c8c8c19cfdc12ec88545ca1"
98
98
  }
@@ -10,7 +10,8 @@ var _jsonPointer = require("@swaggerexpert/json-pointer");
10
10
  var _empty = require("@speclynx/apidom-reference/configuration/empty");
11
11
  var _OpenAPIDocument = _interopRequireDefault(require("../document/OpenAPIDocument.cjs"));
12
12
  var _RuntimeExpressionEvaluator = _interopRequireDefault(require("../expression/RuntimeExpressionEvaluator.cjs"));
13
- var _ExtractionError = _interopRequireDefault(require("../errors/ExtractionError.cjs"));
13
+ var _OpenAPIOperationExtractor = _interopRequireDefault(require("../extractor/OpenAPIOperationExtractor.cjs"));
14
+ var _ExecutionError = _interopRequireDefault(require("../errors/ExecutionError.cjs"));
14
15
  var _SourceDescriptionsLocatorNormalizer = _interopRequireDefault(require("./SourceDescriptionsLocatorNormalizer.cjs"));
15
16
  /**
16
17
  * A canonical OpenAPI operation locator: the OpenAPI document that owns the
@@ -34,7 +35,7 @@ var _SourceDescriptionsLocatorNormalizer = _interopRequireDefault(require("./Sou
34
35
  * `$sourceDescriptions.{name}.{operationId}`. Per the Arazzo Specification, a
35
36
  * plain name is only valid when a single (non-arazzo) source description is
36
37
  * defined; multiple sources require the expression form. A plain name is
37
- * therefore resolved against the first source description that has it.
38
+ * therefore looked up in the first OpenAPI source description only.
38
39
  * - `operationPath` embeds a `{$sourceDescriptions.{name}.url}` runtime
39
40
  * expression followed by a JSON Pointer to the operation.
40
41
  *
@@ -42,21 +43,31 @@ var _SourceDescriptionsLocatorNormalizer = _interopRequireDefault(require("./Sou
42
43
  * or wrong); a source is classified by what it actually parses to. The
43
44
  * normalizer acquires the referenced source documents from the registry on
44
45
  * demand.
46
+ *
47
+ * A reference that cannot be resolved throws {@link ExecutionError}, attributed
48
+ * to the referencing step when a {@link StepScope} is given:
49
+ * `operation-not-found` when no operation answers it,
50
+ * `source-description-not-found` for a source name the document does not
51
+ * define, `source-description-not-openapi` for a source that is not an OpenAPI
52
+ * document, and `invalid-operation-reference` for a reference malformed as
53
+ * written. Every returned locator names an existing operation.
45
54
  * @public
46
55
  */
47
56
  class OpenAPIOperationLocatorNormalizer extends _SourceDescriptionsLocatorNormalizer.default {
57
+ #extractor = new _OpenAPIOperationExtractor.default();
58
+
48
59
  /**
49
60
  * Normalizes a plain or expression `operationId` to a canonical locator.
50
61
  *
51
62
  * A whole runtime expression (`$sourceDescriptions.{name}.{operationId}`) is
52
- * resolved against its named source; a plain name is resolved against the
53
- * first source description that has it.
63
+ * resolved against its named source; a plain name is looked up in the first
64
+ * OpenAPI source description only.
54
65
  */
55
- async normalizeOperationId(operationId, document) {
66
+ async normalizeOperationId(operationId, document, scope) {
56
67
  if ((0, _arazzoRuntimeExpression.test)(operationId)) {
57
- return this.#resolveSourced(operationId, document);
68
+ return this.#resolveSourced(operationId, document, scope);
58
69
  }
59
- return this.#resolvePlainOperationId(operationId, document);
70
+ return this.#resolvePlainOperationId(operationId, document, scope);
60
71
  }
61
72
 
62
73
  /**
@@ -72,10 +83,21 @@ class OpenAPIOperationLocatorNormalizer extends _SourceDescriptionsLocatorNormal
72
83
  * The runner only needs a URL and a JSON Pointer, so it does not enforce that
73
84
  * the source description is identified by a runtime expression rather than a
74
85
  * literal URL (a literal URL interpolates to itself and still resolves) — that
75
- * is a conformance concern for the validator. It fails only when the URL does
76
- * not resolve to an OpenAPI source or the JSON Pointer is invalid.
86
+ * is a conformance concern for the validator. A source name the document does
87
+ * not define is detected before interpolation, so it is reported as such
88
+ * rather than as the empty URL it would interpolate to; the JSON Pointer must
89
+ * locate an operation in the acquired OpenAPI document.
77
90
  */
78
- async normalizeOperationPath(operationPath, document) {
91
+ async normalizeOperationPath(operationPath, document, scope) {
92
+ for (const expression of (0, _arazzoRuntimeExpression.extract)(operationPath) ?? []) {
93
+ const parsed = this.parseSourceDescriptionsExpression(expression);
94
+ if (parsed !== undefined && document.resolveSourceDescriptionURI(parsed.sourceName) === undefined) {
95
+ throw new _ExecutionError.default(`operationPath "${operationPath}" names source description "${parsed.sourceName}", which the Arazzo document at "${document.uri}" does not define`, {
96
+ ...scope,
97
+ reason: 'source-description-not-found'
98
+ });
99
+ }
100
+ }
79
101
  const evaluator = new _RuntimeExpressionEvaluator.default({}, {
80
102
  strict: false,
81
103
  document,
@@ -84,23 +106,35 @@ class OpenAPIOperationLocatorNormalizer extends _SourceDescriptionsLocatorNormal
84
106
  const uriReference = evaluator.interpolate(operationPath);
85
107
  const documentURI = _empty.url.sanitize(_empty.url.stripHash(uriReference));
86
108
  if (documentURI === '') {
87
- throw new _ExtractionError.default(`operationPath "${operationPath}" does not resolve to a source description URL`, {
88
- pointer: operationPath,
89
- uri: document.uri
109
+ throw new _ExecutionError.default(`operationPath "${operationPath}" does not resolve to a source description URL`, {
110
+ ...scope,
111
+ reason: 'invalid-operation-reference'
90
112
  });
91
113
  }
92
114
  const jsonPointer = _jsonPointer.URIFragmentIdentifier.fromURIReference(uriReference);
93
115
  if (!(0, _jsonPointer.testJSONPointer)(jsonPointer)) {
94
- throw new _ExtractionError.default(`operationPath "${operationPath}" has an invalid JSON Pointer`, {
95
- pointer: operationPath,
96
- uri: document.uri
116
+ throw new _ExecutionError.default(`operationPath "${operationPath}" has an invalid JSON Pointer`, {
117
+ ...scope,
118
+ reason: 'invalid-operation-reference'
97
119
  });
98
120
  }
99
121
  const source = await this.registry.acquire(documentURI);
100
122
  if (!_OpenAPIDocument.default.is(source)) {
101
- throw new _ExtractionError.default(`operationPath "${operationPath}" does not reference an OpenAPI document`, {
102
- pointer: operationPath,
103
- uri: documentURI
123
+ throw new _ExecutionError.default(`operationPath "${operationPath}" references "${documentURI}", which is not an OpenAPI document`, {
124
+ ...scope,
125
+ reason: 'source-description-not-openapi'
126
+ });
127
+ }
128
+
129
+ // the extractor owns locating an operation by pointer; its failure here
130
+ // means the pointer names no operation in this document.
131
+ try {
132
+ this.#extractor.extractByPointer(source, jsonPointer);
133
+ } catch (error) {
134
+ throw new _ExecutionError.default(`operationPath "${operationPath}" does not locate an operation in the OpenAPI document at "${source.uri}"`, {
135
+ cause: error,
136
+ ...scope,
137
+ reason: 'operation-not-found'
104
138
  });
105
139
  }
106
140
  return {
@@ -108,31 +142,24 @@ class OpenAPIOperationLocatorNormalizer extends _SourceDescriptionsLocatorNormal
108
142
  jsonPointer
109
143
  };
110
144
  }
111
-
112
- /**
113
- * Parses a `$sourceDescriptions.{name}.{reference}` expression, asserting
114
- * it is a source-descriptions expression.
115
- */
116
- #parseSourceDescriptionsExpression(expression) {
145
+ async #resolveSourced(expression, document, scope) {
117
146
  const parsed = this.parseSourceDescriptionsExpression(expression);
118
147
  if (parsed === undefined) {
119
- throw new _ExtractionError.default(`Unsupported operationId expression "${expression}"`, {
120
- operationId: expression
148
+ throw new _ExecutionError.default(`operationId "${expression}" is not a $sourceDescriptions.{name}.{operationId} expression`, {
149
+ ...scope,
150
+ reason: 'invalid-operation-reference'
121
151
  });
122
152
  }
123
- return parsed;
124
- }
125
- async #resolveSourced(expression, document) {
126
153
  const {
127
154
  sourceName,
128
155
  reference: operationId
129
- } = this.#parseSourceDescriptionsExpression(expression);
130
- const openapiDocument = await this.#sourceDocument(sourceName, document);
156
+ } = parsed;
157
+ const openapiDocument = await this.#sourceDocument(sourceName, document, scope);
131
158
  const jsonPointer = openapiDocument.operationIndex.get(operationId);
132
159
  if (jsonPointer === undefined) {
133
- throw new _ExtractionError.default(`Operation "${operationId}" not found in source description "${sourceName}"`, {
134
- operationId,
135
- uri: openapiDocument.uri
160
+ throw new _ExecutionError.default(`Operation "${operationId}" not found in source description "${sourceName}" at "${openapiDocument.uri}"`, {
161
+ ...scope,
162
+ reason: 'operation-not-found'
136
163
  });
137
164
  }
138
165
  return {
@@ -153,15 +180,15 @@ class OpenAPIOperationLocatorNormalizer extends _SourceDescriptionsLocatorNormal
153
180
  * Non-OpenAPI (arazzo) sources are skipped. Sources are acquired one at a time
154
181
  * until the first OpenAPI source is reached.
155
182
  */
156
- async #resolvePlainOperationId(operationId, document) {
183
+ async #resolvePlainOperationId(operationId, document, scope) {
157
184
  for (const uri of document.sourceDescriptionURIs()) {
158
185
  const source = await this.registry.acquire(uri);
159
186
  if (!_OpenAPIDocument.default.is(source)) continue;
160
187
  const jsonPointer = source.operationIndex.get(operationId);
161
188
  if (jsonPointer === undefined) {
162
- throw new _ExtractionError.default(`Operation "${operationId}" not found in source description at "${source.uri}"`, {
163
- operationId,
164
- uri: source.uri
189
+ throw new _ExecutionError.default(`Operation "${operationId}" not found in source description at "${source.uri}"`, {
190
+ ...scope,
191
+ reason: 'operation-not-found'
165
192
  });
166
193
  }
167
194
  return {
@@ -169,26 +196,31 @@ class OpenAPIOperationLocatorNormalizer extends _SourceDescriptionsLocatorNormal
169
196
  jsonPointer
170
197
  };
171
198
  }
172
- throw new _ExtractionError.default(`Operation "${operationId}" has no OpenAPI source description`, {
173
- operationId,
174
- uri: document.uri
199
+
200
+ // no source was named, so this is not a missing source description: there
201
+ // is simply no OpenAPI document for the operation to be in.
202
+ throw new _ExecutionError.default(`Operation "${operationId}" not found: the Arazzo document at "${document.uri}" has no OpenAPI source description`, {
203
+ ...scope,
204
+ reason: 'operation-not-found'
175
205
  });
176
206
  }
177
207
 
178
208
  /**
179
209
  * Acquires a source description by name and asserts it is an OpenAPI document.
180
210
  */
181
- async #sourceDocument(sourceName, document) {
211
+ async #sourceDocument(sourceName, document, scope) {
182
212
  const uri = document.resolveSourceDescriptionURI(sourceName);
183
213
  if (uri === undefined) {
184
- throw new _ExtractionError.default(`Source description "${sourceName}" not found`, {
185
- uri: document.uri
214
+ throw new _ExecutionError.default(`Source description "${sourceName}" not found in the Arazzo document at "${document.uri}"`, {
215
+ ...scope,
216
+ reason: 'source-description-not-found'
186
217
  });
187
218
  }
188
219
  const source = await this.registry.acquire(uri);
189
220
  if (!_OpenAPIDocument.default.is(source)) {
190
- throw new _ExtractionError.default(`Source description "${sourceName}" is not an OpenAPI document`, {
191
- uri
221
+ throw new _ExecutionError.default(`Source description "${sourceName}" at "${uri}" is not an OpenAPI document`, {
222
+ ...scope,
223
+ reason: 'source-description-not-openapi'
192
224
  });
193
225
  }
194
226
  return source;
@@ -1,9 +1,10 @@
1
- import { test as isRuntimeExpression } from '@swaggerexpert/arazzo-runtime-expression';
1
+ import { extract, test as isRuntimeExpression } from '@swaggerexpert/arazzo-runtime-expression';
2
2
  import { URIFragmentIdentifier, testJSONPointer } from '@swaggerexpert/json-pointer';
3
3
  import { url } from '@speclynx/apidom-reference/configuration/empty';
4
4
  import OpenAPIDocument from "../document/OpenAPIDocument.mjs";
5
5
  import RuntimeExpressionEvaluator from "../expression/RuntimeExpressionEvaluator.mjs";
6
- import ExtractionError from "../errors/ExtractionError.mjs";
6
+ import OpenAPIOperationExtractor from "../extractor/OpenAPIOperationExtractor.mjs";
7
+ import ExecutionError from "../errors/ExecutionError.mjs";
7
8
  import SourceDescriptionsLocatorNormalizer from "./SourceDescriptionsLocatorNormalizer.mjs";
8
9
  /**
9
10
  * A canonical OpenAPI operation locator: the OpenAPI document that owns the
@@ -26,7 +27,7 @@ import SourceDescriptionsLocatorNormalizer from "./SourceDescriptionsLocatorNorm
26
27
  * `$sourceDescriptions.{name}.{operationId}`. Per the Arazzo Specification, a
27
28
  * plain name is only valid when a single (non-arazzo) source description is
28
29
  * defined; multiple sources require the expression form. A plain name is
29
- * therefore resolved against the first source description that has it.
30
+ * therefore looked up in the first OpenAPI source description only.
30
31
  * - `operationPath` embeds a `{$sourceDescriptions.{name}.url}` runtime
31
32
  * expression followed by a JSON Pointer to the operation.
32
33
  *
@@ -34,21 +35,31 @@ import SourceDescriptionsLocatorNormalizer from "./SourceDescriptionsLocatorNorm
34
35
  * or wrong); a source is classified by what it actually parses to. The
35
36
  * normalizer acquires the referenced source documents from the registry on
36
37
  * demand.
38
+ *
39
+ * A reference that cannot be resolved throws {@link ExecutionError}, attributed
40
+ * to the referencing step when a {@link StepScope} is given:
41
+ * `operation-not-found` when no operation answers it,
42
+ * `source-description-not-found` for a source name the document does not
43
+ * define, `source-description-not-openapi` for a source that is not an OpenAPI
44
+ * document, and `invalid-operation-reference` for a reference malformed as
45
+ * written. Every returned locator names an existing operation.
37
46
  * @public
38
47
  */
39
48
  class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormalizer {
49
+ #extractor = new OpenAPIOperationExtractor();
50
+
40
51
  /**
41
52
  * Normalizes a plain or expression `operationId` to a canonical locator.
42
53
  *
43
54
  * A whole runtime expression (`$sourceDescriptions.{name}.{operationId}`) is
44
- * resolved against its named source; a plain name is resolved against the
45
- * first source description that has it.
55
+ * resolved against its named source; a plain name is looked up in the first
56
+ * OpenAPI source description only.
46
57
  */
47
- async normalizeOperationId(operationId, document) {
58
+ async normalizeOperationId(operationId, document, scope) {
48
59
  if (isRuntimeExpression(operationId)) {
49
- return this.#resolveSourced(operationId, document);
60
+ return this.#resolveSourced(operationId, document, scope);
50
61
  }
51
- return this.#resolvePlainOperationId(operationId, document);
62
+ return this.#resolvePlainOperationId(operationId, document, scope);
52
63
  }
53
64
 
54
65
  /**
@@ -64,10 +75,21 @@ class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormali
64
75
  * The runner only needs a URL and a JSON Pointer, so it does not enforce that
65
76
  * the source description is identified by a runtime expression rather than a
66
77
  * literal URL (a literal URL interpolates to itself and still resolves) — that
67
- * is a conformance concern for the validator. It fails only when the URL does
68
- * not resolve to an OpenAPI source or the JSON Pointer is invalid.
78
+ * is a conformance concern for the validator. A source name the document does
79
+ * not define is detected before interpolation, so it is reported as such
80
+ * rather than as the empty URL it would interpolate to; the JSON Pointer must
81
+ * locate an operation in the acquired OpenAPI document.
69
82
  */
70
- async normalizeOperationPath(operationPath, document) {
83
+ async normalizeOperationPath(operationPath, document, scope) {
84
+ for (const expression of extract(operationPath) ?? []) {
85
+ const parsed = this.parseSourceDescriptionsExpression(expression);
86
+ if (parsed !== undefined && document.resolveSourceDescriptionURI(parsed.sourceName) === undefined) {
87
+ throw new ExecutionError(`operationPath "${operationPath}" names source description "${parsed.sourceName}", which the Arazzo document at "${document.uri}" does not define`, {
88
+ ...scope,
89
+ reason: 'source-description-not-found'
90
+ });
91
+ }
92
+ }
71
93
  const evaluator = new RuntimeExpressionEvaluator({}, {
72
94
  strict: false,
73
95
  document,
@@ -76,23 +98,35 @@ class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormali
76
98
  const uriReference = evaluator.interpolate(operationPath);
77
99
  const documentURI = url.sanitize(url.stripHash(uriReference));
78
100
  if (documentURI === '') {
79
- throw new ExtractionError(`operationPath "${operationPath}" does not resolve to a source description URL`, {
80
- pointer: operationPath,
81
- uri: document.uri
101
+ throw new ExecutionError(`operationPath "${operationPath}" does not resolve to a source description URL`, {
102
+ ...scope,
103
+ reason: 'invalid-operation-reference'
82
104
  });
83
105
  }
84
106
  const jsonPointer = URIFragmentIdentifier.fromURIReference(uriReference);
85
107
  if (!testJSONPointer(jsonPointer)) {
86
- throw new ExtractionError(`operationPath "${operationPath}" has an invalid JSON Pointer`, {
87
- pointer: operationPath,
88
- uri: document.uri
108
+ throw new ExecutionError(`operationPath "${operationPath}" has an invalid JSON Pointer`, {
109
+ ...scope,
110
+ reason: 'invalid-operation-reference'
89
111
  });
90
112
  }
91
113
  const source = await this.registry.acquire(documentURI);
92
114
  if (!OpenAPIDocument.is(source)) {
93
- throw new ExtractionError(`operationPath "${operationPath}" does not reference an OpenAPI document`, {
94
- pointer: operationPath,
95
- uri: documentURI
115
+ throw new ExecutionError(`operationPath "${operationPath}" references "${documentURI}", which is not an OpenAPI document`, {
116
+ ...scope,
117
+ reason: 'source-description-not-openapi'
118
+ });
119
+ }
120
+
121
+ // the extractor owns locating an operation by pointer; its failure here
122
+ // means the pointer names no operation in this document.
123
+ try {
124
+ this.#extractor.extractByPointer(source, jsonPointer);
125
+ } catch (error) {
126
+ throw new ExecutionError(`operationPath "${operationPath}" does not locate an operation in the OpenAPI document at "${source.uri}"`, {
127
+ cause: error,
128
+ ...scope,
129
+ reason: 'operation-not-found'
96
130
  });
97
131
  }
98
132
  return {
@@ -100,31 +134,24 @@ class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormali
100
134
  jsonPointer
101
135
  };
102
136
  }
103
-
104
- /**
105
- * Parses a `$sourceDescriptions.{name}.{reference}` expression, asserting
106
- * it is a source-descriptions expression.
107
- */
108
- #parseSourceDescriptionsExpression(expression) {
137
+ async #resolveSourced(expression, document, scope) {
109
138
  const parsed = this.parseSourceDescriptionsExpression(expression);
110
139
  if (parsed === undefined) {
111
- throw new ExtractionError(`Unsupported operationId expression "${expression}"`, {
112
- operationId: expression
140
+ throw new ExecutionError(`operationId "${expression}" is not a $sourceDescriptions.{name}.{operationId} expression`, {
141
+ ...scope,
142
+ reason: 'invalid-operation-reference'
113
143
  });
114
144
  }
115
- return parsed;
116
- }
117
- async #resolveSourced(expression, document) {
118
145
  const {
119
146
  sourceName,
120
147
  reference: operationId
121
- } = this.#parseSourceDescriptionsExpression(expression);
122
- const openapiDocument = await this.#sourceDocument(sourceName, document);
148
+ } = parsed;
149
+ const openapiDocument = await this.#sourceDocument(sourceName, document, scope);
123
150
  const jsonPointer = openapiDocument.operationIndex.get(operationId);
124
151
  if (jsonPointer === undefined) {
125
- throw new ExtractionError(`Operation "${operationId}" not found in source description "${sourceName}"`, {
126
- operationId,
127
- uri: openapiDocument.uri
152
+ throw new ExecutionError(`Operation "${operationId}" not found in source description "${sourceName}" at "${openapiDocument.uri}"`, {
153
+ ...scope,
154
+ reason: 'operation-not-found'
128
155
  });
129
156
  }
130
157
  return {
@@ -145,15 +172,15 @@ class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormali
145
172
  * Non-OpenAPI (arazzo) sources are skipped. Sources are acquired one at a time
146
173
  * until the first OpenAPI source is reached.
147
174
  */
148
- async #resolvePlainOperationId(operationId, document) {
175
+ async #resolvePlainOperationId(operationId, document, scope) {
149
176
  for (const uri of document.sourceDescriptionURIs()) {
150
177
  const source = await this.registry.acquire(uri);
151
178
  if (!OpenAPIDocument.is(source)) continue;
152
179
  const jsonPointer = source.operationIndex.get(operationId);
153
180
  if (jsonPointer === undefined) {
154
- throw new ExtractionError(`Operation "${operationId}" not found in source description at "${source.uri}"`, {
155
- operationId,
156
- uri: source.uri
181
+ throw new ExecutionError(`Operation "${operationId}" not found in source description at "${source.uri}"`, {
182
+ ...scope,
183
+ reason: 'operation-not-found'
157
184
  });
158
185
  }
159
186
  return {
@@ -161,26 +188,31 @@ class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormali
161
188
  jsonPointer
162
189
  };
163
190
  }
164
- throw new ExtractionError(`Operation "${operationId}" has no OpenAPI source description`, {
165
- operationId,
166
- uri: document.uri
191
+
192
+ // no source was named, so this is not a missing source description: there
193
+ // is simply no OpenAPI document for the operation to be in.
194
+ throw new ExecutionError(`Operation "${operationId}" not found: the Arazzo document at "${document.uri}" has no OpenAPI source description`, {
195
+ ...scope,
196
+ reason: 'operation-not-found'
167
197
  });
168
198
  }
169
199
 
170
200
  /**
171
201
  * Acquires a source description by name and asserts it is an OpenAPI document.
172
202
  */
173
- async #sourceDocument(sourceName, document) {
203
+ async #sourceDocument(sourceName, document, scope) {
174
204
  const uri = document.resolveSourceDescriptionURI(sourceName);
175
205
  if (uri === undefined) {
176
- throw new ExtractionError(`Source description "${sourceName}" not found`, {
177
- uri: document.uri
206
+ throw new ExecutionError(`Source description "${sourceName}" not found in the Arazzo document at "${document.uri}"`, {
207
+ ...scope,
208
+ reason: 'source-description-not-found'
178
209
  });
179
210
  }
180
211
  const source = await this.registry.acquire(uri);
181
212
  if (!OpenAPIDocument.is(source)) {
182
- throw new ExtractionError(`Source description "${sourceName}" is not an OpenAPI document`, {
183
- uri
213
+ throw new ExecutionError(`Source description "${sourceName}" at "${uri}" is not an OpenAPI document`, {
214
+ ...scope,
215
+ reason: 'source-description-not-openapi'
184
216
  });
185
217
  }
186
218
  return source;
@@ -134,38 +134,45 @@ class StepExecutor {
134
134
  * request nobody is waiting for, whose wire-level cancellation would surface
135
135
  * as a transport {@link ClientError} — a request that failed on its own terms
136
136
  * — rather than as the withdrawal it is.
137
+ *
138
+ * `workflowId` names the workflow the step belongs to; every error raised
139
+ * here then carries it alongside the `stepId`, since step ids are only
140
+ * unique within a workflow. The workflow executor always passes it.
137
141
  */
138
- async execute(step, state, executeOptions = {}) {
142
+ async execute(step, state, executeOptions = {}, workflowId) {
139
143
  const stepId = (0, _apidomCore.toValue)(step.stepId);
144
+ const scope = {
145
+ ...(workflowId === undefined ? {} : {
146
+ workflowId
147
+ }),
148
+ stepId
149
+ };
140
150
 
141
151
  // a step's target fields are mutually exclusive; declaring more than one is
142
152
  // malformed and has no defined resolution.
143
153
  const declaredTargets = STEP_TARGET_FIELDS.filter(field => (0, _apidomDatamodel.isStringElement)(step[field]));
144
154
  if (declaredTargets.length > 1) {
145
155
  throw new _ExecutionError.default(`Step "${stepId}" declares more than one of ${STEP_TARGET_FIELDS.join(', ')} (mutually exclusive)`, {
146
- stepId,
156
+ ...scope,
147
157
  reason: 'ambiguous-target'
148
158
  });
149
159
  }
150
160
  if ((0, _apidomDatamodel.isStringElement)(step.workflowId)) {
151
161
  throw new _ExecutionError.default(`Step "${stepId}" targets a workflowId; use the workflow executor`, {
152
- stepId,
162
+ ...scope,
153
163
  reason: 'workflow-step'
154
164
  });
155
165
  }
156
- this.assertSuccessCriteria(step);
166
+ this.assertSuccessCriteria(step, workflowId);
157
167
 
158
168
  // locate which operation the step invokes — reduced to a (document, JSON
159
169
  // Pointer) locator that the operation executor extracts, normalizes,
160
170
  // assembles into a standalone doc, and runs.
161
- const locator = await this.#locateOperation(step, stepId);
171
+ const locator = await this.#locateOperation(step, scope);
162
172
 
163
173
  // resolve parameters and request body against the pre-request context.
164
174
  // keyed by '{in}.{name}' rather than bare name, so two parameters that
165
175
  // legally differ only in their location each reach their own place.
166
- const scope = {
167
- stepId
168
- };
169
176
  const preContext = state.toContext();
170
177
  const parameters = this.#parameterResolver.resolve(step.parameters, expression => this.#evaluate(preContext, expression), scope);
171
178
  const requestBody = this.#requestBodyResolver.resolve(step.requestBody, expression => this.#evaluate(preContext, expression), scope);
@@ -176,9 +183,7 @@ class StepExecutor {
176
183
  // live side effect. The signal stays in the bag, so a request already in
177
184
  // flight is aborted by the transport as before.
178
185
  const signal = (0, _abort.readAbortSignal)(executeOptions);
179
- (0, _abort.throwIfAborted)(signal, {
180
- stepId
181
- });
186
+ (0, _abort.throwIfAborted)(signal, scope);
182
187
 
183
188
  // the Arazzo-derived parameters and request body are spread after the opaque
184
189
  // executeOptions bag so they take precedence over it. the operation executor
@@ -197,22 +202,20 @@ class StepExecutor {
197
202
  // cancelled, it did not: reporting it as the abort keeps one error shape
198
203
  // whether the run was withdrawn between two steps or mid-flight, and keeps
199
204
  // the whole feature from reading differently per transport.
200
- (0, _abort.throwIfAborted)(signal, {
201
- stepId
202
- });
205
+ (0, _abort.throwIfAborted)(signal, scope);
203
206
  throw error;
204
207
  }
205
208
 
206
209
  // evaluate criteria, outputs, and the next action against the post-request
207
210
  // context (with $request / $url / $method and $response / $statusCode).
208
211
  const postContext = state.toContext(this.#requestContext(response), this.#responseContext(response));
209
- const successCriteria = this.evaluateCriteria(step, postContext);
212
+ const successCriteria = this.evaluateCriteria(step, postContext, workflowId);
210
213
  const successful = successCriteria.every(criterion => criterion.met);
211
214
  const outputs = this.#outputResolver.resolve(step.outputs, expression => this.#evaluate(postContext, expression), scope);
212
215
  const {
213
216
  matchedActions,
214
217
  actionCriteria
215
- } = this.selectActions(step, successful, postContext);
218
+ } = this.selectActions(step, successful, postContext, workflowId);
216
219
  return {
217
220
  stepId,
218
221
  response,
@@ -242,15 +245,15 @@ class StepExecutor {
242
245
  requestContentType: requestBody.contentType
243
246
  };
244
247
  }
245
- async #locateOperation(step, stepId) {
248
+ async #locateOperation(step, scope) {
246
249
  if ((0, _apidomDatamodel.isStringElement)(step.operationId)) {
247
- return this.#locatorNormalizer.normalizeOperationId((0, _apidomCore.toValue)(step.operationId), this.#document);
250
+ return this.#locatorNormalizer.normalizeOperationId((0, _apidomCore.toValue)(step.operationId), this.#document, scope);
248
251
  }
249
252
  if ((0, _apidomDatamodel.isStringElement)(step.operationPath)) {
250
- return this.#locatorNormalizer.normalizeOperationPath((0, _apidomCore.toValue)(step.operationPath), this.#document);
253
+ return this.#locatorNormalizer.normalizeOperationPath((0, _apidomCore.toValue)(step.operationPath), this.#document, scope);
251
254
  }
252
- throw new _ExecutionError.default(`Step "${stepId}" has no operationId or operationPath`, {
253
- stepId,
255
+ throw new _ExecutionError.default(`Step "${scope.stepId}" has no operationId or operationPath`, {
256
+ ...scope,
254
257
  reason: 'missing-operation'
255
258
  });
256
259
  }
@@ -304,10 +307,12 @@ class StepExecutor {
304
307
  * Public for the same reason as {@link StepExecutor.selectActions}: a step
305
308
  * targeting a `workflowId` is run by the workflow executor and never reaches
306
309
  * {@link StepExecutor.execute}, but its criteria are still the author's
307
- * assertion about the step and must be evaluated somewhere.
310
+ * assertion about the step and must be evaluated somewhere. `workflowId`
311
+ * names the step's workflow on a thrown error, as for
312
+ * {@link StepExecutor.execute}.
308
313
  */
309
- evaluateCriteria(step, context) {
310
- this.assertSuccessCriteria(step);
314
+ evaluateCriteria(step, context, workflowId) {
315
+ this.assertSuccessCriteria(step, workflowId);
311
316
  const criteria = step.successCriteria;
312
317
  if (criteria === undefined) return [];
313
318
 
@@ -335,8 +340,10 @@ class StepExecutor {
335
340
  * it runs itself and which therefore never reaches
336
341
  * {@link StepExecutor.execute}. Both callers reach the criteria evaluator and
337
342
  * the expression context through this one method rather than wiring their own.
343
+ * `workflowId` names the step's workflow on a thrown error, as for
344
+ * {@link StepExecutor.execute}.
338
345
  */
339
- selectActions(step, successful, context) {
346
+ selectActions(step, successful, context, workflowId) {
340
347
  const actionCriteria = [];
341
348
  const isCriterionMet = (criterion, action) => {
342
349
  const record = {
@@ -347,6 +354,9 @@ class StepExecutor {
347
354
  return record.met;
348
355
  };
349
356
  const scope = {
357
+ ...(workflowId === undefined ? {} : {
358
+ workflowId
359
+ }),
350
360
  stepId: (0, _apidomCore.toValue)(step.stepId)
351
361
  };
352
362
  const matchedActions = successful ? this.#actionResolver.resolveOnSuccess(step.onSuccess, isCriterionMet, scope) : this.#actionResolver.resolveOnFailure(step.onFailure, isCriterionMet, scope);