@jentic/arazzo-parser 1.0.0-alpha.32 → 1.0.0-alpha.33

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": "@jentic/arazzo-parser",
3
- "version": "1.0.0-alpha.32",
3
+ "version": "1.0.0-alpha.33",
4
4
  "description": "Parser for Arazzo Documents producing SpecLynx ApiDOM data model.",
5
5
  "keywords": [
6
6
  "arazzo",
@@ -48,9 +48,14 @@
48
48
  "license": "Apache-2.0",
49
49
  "dependencies": {
50
50
  "@babel/runtime-corejs3": "^8.0.0",
51
+ "@speclynx/apidom-core": "^5.0.1",
51
52
  "@speclynx/apidom-datamodel": "^5.0.1",
52
53
  "@speclynx/apidom-error": "^5.0.1",
54
+ "@speclynx/apidom-json-pointer": "^5.0.1",
53
55
  "@speclynx/apidom-ns-arazzo-1": "^5.0.1",
56
+ "@speclynx/apidom-ns-openapi-2": "^5.0.1",
57
+ "@speclynx/apidom-ns-openapi-3-0": "^5.0.1",
58
+ "@speclynx/apidom-ns-openapi-3-1": "^5.0.1",
54
59
  "@speclynx/apidom-parser-adapter-arazzo-json-1": "^5.0.1",
55
60
  "@speclynx/apidom-parser-adapter-arazzo-yaml-1": "^5.0.1",
56
61
  "@speclynx/apidom-parser-adapter-openapi-json-2": "^5.0.1",
@@ -63,9 +68,6 @@
63
68
  "ramda-adjunct": "^6.0.0",
64
69
  "type-fest": "^5.4.4"
65
70
  },
66
- "devDependencies": {
67
- "@speclynx/apidom-core": "^5.0.1"
68
- },
69
71
  "files": [
70
72
  "src/**/*.mjs",
71
73
  "src/**/*.cjs",
@@ -76,5 +78,5 @@
76
78
  "README.md",
77
79
  "CHANGELOG.md"
78
80
  ],
79
- "gitHead": "a5ea37ac6a1bd3b56df95cdb308a9b62a4cc375b"
81
+ "gitHead": "a17270442ac27615af0348fad9c71817d818151f"
80
82
  }
package/src/index.cjs CHANGED
@@ -3,6 +3,12 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "attachStepOperations", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _stepOperations.attachStepOperations;
10
+ }
11
+ });
6
12
  Object.defineProperty(exports, "defaultParseArazzoOptions", {
7
13
  enumerable: true,
8
14
  get: function () {
@@ -15,6 +21,12 @@ Object.defineProperty(exports, "defaultParseOpenAPIOptions", {
15
21
  return _parseOpenapi.defaultOptions;
16
22
  }
17
23
  });
24
+ Object.defineProperty(exports, "findStepOperation", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _stepOperations.findStepOperation;
28
+ }
29
+ });
18
30
  Object.defineProperty(exports, "parseArazzo", {
19
31
  enumerable: true,
20
32
  get: function () {
@@ -28,4 +40,5 @@ Object.defineProperty(exports, "parseOpenAPI", {
28
40
  }
29
41
  });
30
42
  var _parseArazzo = require("./parse-arazzo.cjs");
43
+ var _stepOperations = require("./step-operations.cjs");
31
44
  var _parseOpenapi = require("./parse-openapi.cjs");
package/src/index.mjs CHANGED
@@ -1,2 +1,3 @@
1
1
  export { parse as parseArazzo, defaultOptions as defaultParseArazzoOptions } from "./parse-arazzo.mjs";
2
+ export { findStepOperation, attachStepOperations } from "./step-operations.mjs";
2
3
  export { parse as parseOpenAPI, defaultOptions as defaultParseOpenAPIOptions } from "./parse-openapi.mjs";
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.attachStepOperations = attachStepOperations;
7
+ exports.findStepOperation = findStepOperation;
8
+ var _apidomCore = require("@speclynx/apidom-core");
9
+ var _apidomJsonPointer = require("@speclynx/apidom-json-pointer");
10
+ var _apidomNsArazzo = require("@speclynx/apidom-ns-arazzo-1");
11
+ var _apidomNsOpenapi = require("@speclynx/apidom-ns-openapi-2");
12
+ var _apidomNsOpenapi2 = require("@speclynx/apidom-ns-openapi-3-0");
13
+ var _apidomNsOpenapi3 = require("@speclynx/apidom-ns-openapi-3-1");
14
+ /**
15
+ * HTTP method and path of the OpenAPI operation targeted by an Arazzo step.
16
+ * @public
17
+ */
18
+
19
+ /**
20
+ * Minimal shape of a plain Arazzo Document needed to attach step operations.
21
+ * @public
22
+ */
23
+
24
+ /**
25
+ * A plain Arazzo Document whose steps may carry the `_operation` they target. Every other field
26
+ * of the document type, and of its workflows and steps, is preserved as is.
27
+ * @public
28
+ */
29
+
30
+ // OpenAPI 3.1 path items extend the OpenAPI 3.0 ones, so isPathItem3Element covers both
31
+ const pathItemOperations = pathItem => ({
32
+ get: pathItem.getField,
33
+ put: pathItem.put,
34
+ post: pathItem.post,
35
+ delete: pathItem.deleteField,
36
+ options: pathItem.options,
37
+ head: pathItem.head,
38
+ patch: pathItem.patch,
39
+ trace: (0, _apidomNsOpenapi2.isPathItemElement)(pathItem) ? pathItem.trace : undefined
40
+ });
41
+
42
+ // resolves a (possibly relative) URI against the document it appears in, as far as possible
43
+ const resolveURI = (uri, baseURI) => {
44
+ try {
45
+ return new URL(uri, baseURI).href;
46
+ } catch {
47
+ return uri;
48
+ }
49
+ };
50
+
51
+ // whether a literal URL targets the document of the source description, as declared or as retrieved
52
+ const targetsSource = (sourceDescription, documentURI, baseURI) => {
53
+ const target = resolveURI(documentURI, baseURI);
54
+ const url = sourceDescription.url?.toValue();
55
+ const retrievalURI = sourceDescription.meta.get('parseResult')?.meta.get('retrievalURI');
56
+ return [url, retrievalURI].some(uri => uri !== undefined && (uri === documentURI || resolveURI(uri, baseURI) === target));
57
+ };
58
+ const pointerTokens = pointer => pointer.split('/').slice(1).map(token => token.replace(/~1/g, '/').replace(/~0/g, '~'));
59
+
60
+ // resolves a JSON Pointer against a source document, so it can target operations of both
61
+ // `paths` entries and reusable Path Items (`#/components/pathItems/...`)
62
+ const operationAtPointer = (api, pointer) => {
63
+ const tokens = pointerTokens(pointer);
64
+ const withPath = method => ({
65
+ method: method.toUpperCase(),
66
+ ...(tokens[0] === 'paths' && {
67
+ path: tokens[1]
68
+ })
69
+ });
70
+ const evaluateAt = parts => {
71
+ try {
72
+ return (0, _apidomJsonPointer.evaluate)(api, (0, _apidomJsonPointer.compile)(parts));
73
+ } catch {
74
+ return undefined;
75
+ }
76
+ };
77
+ const target = evaluateAt(tokens);
78
+ // a pointer to a path item (no method) is unambiguous only when it has a single operation
79
+ if ((0, _apidomNsOpenapi.isPathItemElement)(target) || (0, _apidomNsOpenapi2.isPathItemElement)(target)) {
80
+ const defined = Object.entries(pathItemOperations(target)).filter(([, operation]) => operation);
81
+ return defined.length === 1 ? withPath(defined[0][0]) : undefined;
82
+ }
83
+ const method = tokens[tokens.length - 1];
84
+ const parent = evaluateAt(tokens.slice(0, -1));
85
+ if (!target || !((0, _apidomNsOpenapi.isPathItemElement)(parent) || (0, _apidomNsOpenapi2.isPathItemElement)(parent))) return undefined;
86
+ return Object.keys(pathItemOperations(parent)).includes(method) ? withPath(method) : undefined;
87
+ };
88
+
89
+ /**
90
+ * Finds the OpenAPI operation targeted by an Arazzo step in the `openapi` source descriptions
91
+ * parsed along with the Arazzo Document (see the `sourceDescriptions` parser option).
92
+ * @param parseResult - The result of parsing an Arazzo Document with source descriptions
93
+ * @param step - The step's `operationId` or `operationPath`
94
+ * @returns The operation's HTTP method and path, or undefined when it can't be located
95
+ * @public
96
+ */
97
+ function findStepOperation(parseResult, {
98
+ operationId,
99
+ operationPath
100
+ }) {
101
+ const reference = operationPath ?? operationId;
102
+ if (!reference || !(0, _apidomNsArazzo.isArazzoSpecification1Element)(parseResult.api)) return undefined;
103
+ // `$sourceDescriptions.{name}.{operationId}` and `{$sourceDescriptions.{name}.url}#{pointer}` name their source
104
+ const [, sourceName, qualifiedId] = /^\{?\$sourceDescriptions\.([^.]+)\.(.+)$/.exec(reference) ?? [];
105
+ const pointer = operationPath && _apidomJsonPointer.URIFragmentIdentifier.fromURIReference(operationPath);
106
+ // a literal URL (no `$sourceDescriptions` expression) names its source by the document it targets
107
+ const documentURI = operationPath && !sourceName ? operationPath.split('#')[0].replace(/^\{|\}$/g, '') : '';
108
+ const baseURI = parseResult.meta.get('retrievalURI');
109
+ let found;
110
+ parseResult.api.sourceDescriptions?.forEach(sourceDescription => {
111
+ if (found || !(0, _apidomNsArazzo.isSourceDescriptionElement)(sourceDescription)) return;
112
+ if (sourceDescription.type && sourceDescription.type.toValue() !== 'openapi') return;
113
+ if (sourceName && sourceDescription.name?.toValue() !== sourceName) return;
114
+ // the source must be the one the URL targets, and it is not searched when it failed to load
115
+ if (documentURI && !targetsSource(sourceDescription, documentURI, baseURI)) return;
116
+ const api = sourceDescription.meta.get('parseResult')?.api;
117
+ if (!(0, _apidomNsOpenapi.isSwaggerElement)(api) && !(0, _apidomNsOpenapi2.isOpenApi3_0Element)(api) && !(0, _apidomNsOpenapi3.isOpenApi3_1Element)(api)) return;
118
+ if (pointer) {
119
+ found = operationAtPointer(api, pointer);
120
+ return;
121
+ }
122
+ api.paths?.forEach((pathItem, key) => {
123
+ if (found || !(0, _apidomNsOpenapi.isPathItemElement)(pathItem) && !(0, _apidomNsOpenapi2.isPathItemElement)(pathItem)) return;
124
+ const path = String(key.toValue());
125
+ for (const [method, operation] of Object.entries(pathItemOperations(pathItem))) {
126
+ if (operation?.operationId?.toValue() === (qualifiedId ?? operationId)) {
127
+ found = {
128
+ method: method.toUpperCase(),
129
+ path
130
+ };
131
+ return;
132
+ }
133
+ }
134
+ });
135
+ });
136
+ return found;
137
+ }
138
+
139
+ /**
140
+ * Converts an Arazzo Document element to a plain object and sets `_operation` on every step that
141
+ * targets an operation of the `openapi` source descriptions parsed along with it.
142
+ * @param element - The Arazzo Document element, e.g. the (dereferenced) result of the parse result
143
+ * @param parseResult - The result of parsing the Arazzo Document with source descriptions
144
+ * @returns The plain Arazzo Document, typed with `_operation` on its steps and set on the resolved ones
145
+ * @public
146
+ */
147
+ function attachStepOperations(element, parseResult) {
148
+ const doc = (0, _apidomCore.toValue)(element);
149
+ doc?.workflows?.forEach(workflow => workflow.steps?.forEach(step => {
150
+ const operation = findStepOperation(parseResult, step);
151
+ if (operation) Object.assign(step, {
152
+ _operation: operation
153
+ });
154
+ }));
155
+ return doc;
156
+ }
@@ -0,0 +1,150 @@
1
+ import { toValue } from '@speclynx/apidom-core';
2
+ import { compile, evaluate, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
3
+ import { isArazzoSpecification1Element, isSourceDescriptionElement } from '@speclynx/apidom-ns-arazzo-1';
4
+ import { isSwaggerElement, isPathItemElement as isPathItem2Element } from '@speclynx/apidom-ns-openapi-2';
5
+ import { isOpenApi3_0Element, isPathItemElement as isPathItem3Element } from '@speclynx/apidom-ns-openapi-3-0';
6
+ import { isOpenApi3_1Element } from '@speclynx/apidom-ns-openapi-3-1';
7
+
8
+ /**
9
+ * HTTP method and path of the OpenAPI operation targeted by an Arazzo step.
10
+ * @public
11
+ */
12
+
13
+ /**
14
+ * Minimal shape of a plain Arazzo Document needed to attach step operations.
15
+ * @public
16
+ */
17
+
18
+ /**
19
+ * A plain Arazzo Document whose steps may carry the `_operation` they target. Every other field
20
+ * of the document type, and of its workflows and steps, is preserved as is.
21
+ * @public
22
+ */
23
+
24
+ // OpenAPI 3.1 path items extend the OpenAPI 3.0 ones, so isPathItem3Element covers both
25
+ const pathItemOperations = pathItem => ({
26
+ get: pathItem.getField,
27
+ put: pathItem.put,
28
+ post: pathItem.post,
29
+ delete: pathItem.deleteField,
30
+ options: pathItem.options,
31
+ head: pathItem.head,
32
+ patch: pathItem.patch,
33
+ trace: isPathItem3Element(pathItem) ? pathItem.trace : undefined
34
+ });
35
+
36
+ // resolves a (possibly relative) URI against the document it appears in, as far as possible
37
+ const resolveURI = (uri, baseURI) => {
38
+ try {
39
+ return new URL(uri, baseURI).href;
40
+ } catch {
41
+ return uri;
42
+ }
43
+ };
44
+
45
+ // whether a literal URL targets the document of the source description, as declared or as retrieved
46
+ const targetsSource = (sourceDescription, documentURI, baseURI) => {
47
+ const target = resolveURI(documentURI, baseURI);
48
+ const url = sourceDescription.url?.toValue();
49
+ const retrievalURI = sourceDescription.meta.get('parseResult')?.meta.get('retrievalURI');
50
+ return [url, retrievalURI].some(uri => uri !== undefined && (uri === documentURI || resolveURI(uri, baseURI) === target));
51
+ };
52
+ const pointerTokens = pointer => pointer.split('/').slice(1).map(token => token.replace(/~1/g, '/').replace(/~0/g, '~'));
53
+
54
+ // resolves a JSON Pointer against a source document, so it can target operations of both
55
+ // `paths` entries and reusable Path Items (`#/components/pathItems/...`)
56
+ const operationAtPointer = (api, pointer) => {
57
+ const tokens = pointerTokens(pointer);
58
+ const withPath = method => ({
59
+ method: method.toUpperCase(),
60
+ ...(tokens[0] === 'paths' && {
61
+ path: tokens[1]
62
+ })
63
+ });
64
+ const evaluateAt = parts => {
65
+ try {
66
+ return evaluate(api, compile(parts));
67
+ } catch {
68
+ return undefined;
69
+ }
70
+ };
71
+ const target = evaluateAt(tokens);
72
+ // a pointer to a path item (no method) is unambiguous only when it has a single operation
73
+ if (isPathItem2Element(target) || isPathItem3Element(target)) {
74
+ const defined = Object.entries(pathItemOperations(target)).filter(([, operation]) => operation);
75
+ return defined.length === 1 ? withPath(defined[0][0]) : undefined;
76
+ }
77
+ const method = tokens[tokens.length - 1];
78
+ const parent = evaluateAt(tokens.slice(0, -1));
79
+ if (!target || !(isPathItem2Element(parent) || isPathItem3Element(parent))) return undefined;
80
+ return Object.keys(pathItemOperations(parent)).includes(method) ? withPath(method) : undefined;
81
+ };
82
+
83
+ /**
84
+ * Finds the OpenAPI operation targeted by an Arazzo step in the `openapi` source descriptions
85
+ * parsed along with the Arazzo Document (see the `sourceDescriptions` parser option).
86
+ * @param parseResult - The result of parsing an Arazzo Document with source descriptions
87
+ * @param step - The step's `operationId` or `operationPath`
88
+ * @returns The operation's HTTP method and path, or undefined when it can't be located
89
+ * @public
90
+ */
91
+ export function findStepOperation(parseResult, {
92
+ operationId,
93
+ operationPath
94
+ }) {
95
+ const reference = operationPath ?? operationId;
96
+ if (!reference || !isArazzoSpecification1Element(parseResult.api)) return undefined;
97
+ // `$sourceDescriptions.{name}.{operationId}` and `{$sourceDescriptions.{name}.url}#{pointer}` name their source
98
+ const [, sourceName, qualifiedId] = /^\{?\$sourceDescriptions\.([^.]+)\.(.+)$/.exec(reference) ?? [];
99
+ const pointer = operationPath && URIFragmentIdentifier.fromURIReference(operationPath);
100
+ // a literal URL (no `$sourceDescriptions` expression) names its source by the document it targets
101
+ const documentURI = operationPath && !sourceName ? operationPath.split('#')[0].replace(/^\{|\}$/g, '') : '';
102
+ const baseURI = parseResult.meta.get('retrievalURI');
103
+ let found;
104
+ parseResult.api.sourceDescriptions?.forEach(sourceDescription => {
105
+ if (found || !isSourceDescriptionElement(sourceDescription)) return;
106
+ if (sourceDescription.type && sourceDescription.type.toValue() !== 'openapi') return;
107
+ if (sourceName && sourceDescription.name?.toValue() !== sourceName) return;
108
+ // the source must be the one the URL targets, and it is not searched when it failed to load
109
+ if (documentURI && !targetsSource(sourceDescription, documentURI, baseURI)) return;
110
+ const api = sourceDescription.meta.get('parseResult')?.api;
111
+ if (!isSwaggerElement(api) && !isOpenApi3_0Element(api) && !isOpenApi3_1Element(api)) return;
112
+ if (pointer) {
113
+ found = operationAtPointer(api, pointer);
114
+ return;
115
+ }
116
+ api.paths?.forEach((pathItem, key) => {
117
+ if (found || !isPathItem2Element(pathItem) && !isPathItem3Element(pathItem)) return;
118
+ const path = String(key.toValue());
119
+ for (const [method, operation] of Object.entries(pathItemOperations(pathItem))) {
120
+ if (operation?.operationId?.toValue() === (qualifiedId ?? operationId)) {
121
+ found = {
122
+ method: method.toUpperCase(),
123
+ path
124
+ };
125
+ return;
126
+ }
127
+ }
128
+ });
129
+ });
130
+ return found;
131
+ }
132
+
133
+ /**
134
+ * Converts an Arazzo Document element to a plain object and sets `_operation` on every step that
135
+ * targets an operation of the `openapi` source descriptions parsed along with it.
136
+ * @param element - The Arazzo Document element, e.g. the (dereferenced) result of the parse result
137
+ * @param parseResult - The result of parsing the Arazzo Document with source descriptions
138
+ * @returns The plain Arazzo Document, typed with `_operation` on its steps and set on the resolved ones
139
+ * @public
140
+ */
141
+ export function attachStepOperations(element, parseResult) {
142
+ const doc = toValue(element);
143
+ doc?.workflows?.forEach(workflow => workflow.steps?.forEach(step => {
144
+ const operation = findStepOperation(parseResult, step);
145
+ if (operation) Object.assign(step, {
146
+ _operation: operation
147
+ });
148
+ }));
149
+ return doc;
150
+ }
@@ -1,7 +1,18 @@
1
1
  import type { ApiDOMReferenceOptions } from '@speclynx/apidom-reference/configuration/empty';
2
+ import type { Element as Element_2 } from '@speclynx/apidom-datamodel';
2
3
  import { ParseResultElement } from '@speclynx/apidom-datamodel';
3
4
  import type { PartialDeep } from 'type-fest';
4
5
 
6
+ /**
7
+ * Converts an Arazzo Document element to a plain object and sets `_operation` on every step that
8
+ * targets an operation of the `openapi` source descriptions parsed along with it.
9
+ * @param element - The Arazzo Document element, e.g. the (dereferenced) result of the parse result
10
+ * @param parseResult - The result of parsing the Arazzo Document with source descriptions
11
+ * @returns The plain Arazzo Document, typed with `_operation` on its steps and set on the resolved ones
12
+ * @public
13
+ */
14
+ export declare function attachStepOperations<T extends StepOperationsDocument = StepOperationsDocument>(element: Element_2 | undefined, parseResult: ParseResultElement): StepOperationsResult<T>;
15
+
5
16
  /**
6
17
  * Default reference options for parsing Arazzo Documents.
7
18
  * @public
@@ -14,6 +25,19 @@ export declare const defaultParseArazzoOptions: ParseArazzoOptions;
14
25
  */
15
26
  export declare const defaultParseOpenAPIOptions: ParseOpenAPIOptions;
16
27
 
28
+ /**
29
+ * Finds the OpenAPI operation targeted by an Arazzo step in the `openapi` source descriptions
30
+ * parsed along with the Arazzo Document (see the `sourceDescriptions` parser option).
31
+ * @param parseResult - The result of parsing an Arazzo Document with source descriptions
32
+ * @param step - The step's `operationId` or `operationPath`
33
+ * @returns The operation's HTTP method and path, or undefined when it can't be located
34
+ * @public
35
+ */
36
+ export declare function findStepOperation(parseResult: ParseResultElement, { operationId, operationPath }: {
37
+ operationId?: string;
38
+ operationPath?: string;
39
+ }): StepOperation | undefined;
40
+
17
41
  /**
18
42
  * Parses an Arazzo Document from an object.
19
43
  * @param source - The Arazzo Document as a plain object
@@ -88,4 +112,40 @@ export declare type ParseOpenAPIOptions = PartialDeep<ApiDOMReferenceOptions>;
88
112
 
89
113
  export { ParseResultElement }
90
114
 
115
+ /**
116
+ * HTTP method and path of the OpenAPI operation targeted by an Arazzo step.
117
+ * @public
118
+ */
119
+ export declare interface StepOperation {
120
+ /** upper-case HTTP method, e.g. `GET` */
121
+ method: string;
122
+ path?: string;
123
+ }
124
+
125
+ /**
126
+ * Minimal shape of a plain Arazzo Document needed to attach step operations.
127
+ * @public
128
+ */
129
+ export declare interface StepOperationsDocument {
130
+ workflows?: {
131
+ steps?: {
132
+ operationId?: string;
133
+ operationPath?: string;
134
+ }[];
135
+ }[];
136
+ }
137
+
138
+ /**
139
+ * A plain Arazzo Document whose steps may carry the `_operation` they target. Every other field
140
+ * of the document type, and of its workflows and steps, is preserved as is.
141
+ * @public
142
+ */
143
+ export declare type StepOperationsResult<T extends StepOperationsDocument = StepOperationsDocument> = {
144
+ [K in keyof T]: K extends 'workflows' ? NonNullable<T[K]> extends readonly (infer Workflow)[] ? {
145
+ [W in keyof Workflow]: W extends 'steps' ? NonNullable<Workflow[W]> extends readonly (infer Step)[] ? (Step & {
146
+ _operation?: StepOperation;
147
+ })[] : Workflow[W] : Workflow[W];
148
+ }[] : T[K] : T[K];
149
+ };
150
+
91
151
  export { }