@unispechq/unispec-core 0.1.2 → 0.2.2
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/README.md +223 -223
- package/dist/cjs/converters/index.js +206 -0
- package/dist/cjs/diff/index.js +236 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/loader/index.js +22 -0
- package/dist/cjs/normalizer/index.js +107 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/types/index.js +3 -0
- package/dist/cjs/validator/index.js +81 -0
- package/dist/converters/index.js +135 -23
- package/dist/diff/index.js +80 -42
- package/dist/index.cjs +22 -0
- package/dist/normalizer/index.js +41 -53
- package/dist/types/index.d.ts +161 -20
- package/dist/types/index.js +1 -0
- package/dist/validator/index.d.ts +5 -1
- package/dist/validator/index.js +32 -5
- package/package.json +12 -3
package/dist/diff/index.js
CHANGED
|
@@ -34,8 +34,54 @@ function diffValues(oldVal, newVal, basePath, out) {
|
|
|
34
34
|
}
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
-
// Arrays
|
|
37
|
+
// Arrays
|
|
38
38
|
if (Array.isArray(oldVal) && Array.isArray(newVal)) {
|
|
39
|
+
// Special handling for UniSpec collections identified by "name"
|
|
40
|
+
const isNamedCollection = basePath === "/service/protocols/rest/routes" ||
|
|
41
|
+
basePath === "/service/protocols/websocket/channels" ||
|
|
42
|
+
basePath === "/service/protocols/graphql/queries" ||
|
|
43
|
+
basePath === "/service/protocols/graphql/mutations" ||
|
|
44
|
+
basePath === "/service/protocols/graphql/subscriptions";
|
|
45
|
+
if (isNamedCollection) {
|
|
46
|
+
const oldByName = new Map();
|
|
47
|
+
const newByName = new Map();
|
|
48
|
+
for (const item of oldVal) {
|
|
49
|
+
if (item && typeof item === "object" && typeof item.name === "string") {
|
|
50
|
+
oldByName.set(item.name, item);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const item of newVal) {
|
|
54
|
+
if (item && typeof item === "object" && typeof item.name === "string") {
|
|
55
|
+
newByName.set(item.name, item);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Removed
|
|
59
|
+
for (const [name, oldItem] of oldByName.entries()) {
|
|
60
|
+
if (!newByName.has(name)) {
|
|
61
|
+
out.push({
|
|
62
|
+
path: `${basePath}/${name}`,
|
|
63
|
+
description: "Item removed",
|
|
64
|
+
severity: "unknown",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const newItem = newByName.get(name);
|
|
69
|
+
diffValues(oldItem, newItem, `${basePath}/${name}`, out);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Added
|
|
73
|
+
for (const [name] of newByName.entries()) {
|
|
74
|
+
if (!oldByName.has(name)) {
|
|
75
|
+
out.push({
|
|
76
|
+
path: `${basePath}/${name}`,
|
|
77
|
+
description: "Item added",
|
|
78
|
+
severity: "unknown",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Generic shallow index-based compare
|
|
39
85
|
const maxLen = Math.max(oldVal.length, newVal.length);
|
|
40
86
|
for (let i = 0; i < maxLen; i++) {
|
|
41
87
|
const childPath = `${basePath}/${i}`;
|
|
@@ -67,40 +113,29 @@ function diffValues(oldVal, newVal, basePath, out) {
|
|
|
67
113
|
});
|
|
68
114
|
}
|
|
69
115
|
function annotateRestChange(change) {
|
|
70
|
-
if (!change.path.startsWith("/service/protocols/rest/
|
|
116
|
+
if (!change.path.startsWith("/service/protocols/rest/routes/")) {
|
|
71
117
|
return change;
|
|
72
118
|
}
|
|
73
119
|
const segments = change.path.split("/").filter(Boolean);
|
|
74
|
-
// Expected shape: ["service", "protocols", "rest", "
|
|
75
|
-
if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "rest" || segments[3] !== "
|
|
120
|
+
// Expected shape: ["service", "protocols", "rest", "routes", index]
|
|
121
|
+
if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "rest" || segments[3] !== "routes") {
|
|
122
|
+
return change;
|
|
123
|
+
}
|
|
124
|
+
const index = segments[4];
|
|
125
|
+
if (typeof index === "undefined") {
|
|
76
126
|
return change;
|
|
77
127
|
}
|
|
78
|
-
const pathKey = segments[4];
|
|
79
|
-
const method = segments[5];
|
|
80
|
-
const httpMethods = new Set(["get", "head", "options", "post", "put", "patch", "delete"]);
|
|
81
128
|
const annotated = {
|
|
82
129
|
...change,
|
|
83
130
|
protocol: "rest",
|
|
84
131
|
};
|
|
85
|
-
if (change.description === "Field removed") {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
annotated.severity = "breaking";
|
|
89
|
-
}
|
|
90
|
-
else if (pathKey && method && httpMethods.has(method)) {
|
|
91
|
-
annotated.kind = "rest.operation.removed";
|
|
92
|
-
annotated.severity = "breaking";
|
|
93
|
-
}
|
|
132
|
+
if (change.description === "Item removed" || change.description === "Field removed") {
|
|
133
|
+
annotated.kind = "rest.route.removed";
|
|
134
|
+
annotated.severity = "breaking";
|
|
94
135
|
}
|
|
95
|
-
else if (change.description === "Field added") {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
annotated.severity = "non-breaking";
|
|
99
|
-
}
|
|
100
|
-
else if (pathKey && method && httpMethods.has(method)) {
|
|
101
|
-
annotated.kind = "rest.operation.added";
|
|
102
|
-
annotated.severity = "non-breaking";
|
|
103
|
-
}
|
|
136
|
+
else if (change.description === "Item added" || change.description === "Field added") {
|
|
137
|
+
annotated.kind = "rest.route.added";
|
|
138
|
+
annotated.severity = "non-breaking";
|
|
104
139
|
}
|
|
105
140
|
return annotated;
|
|
106
141
|
}
|
|
@@ -109,35 +144,35 @@ function annotateWebSocketChange(change) {
|
|
|
109
144
|
return change;
|
|
110
145
|
}
|
|
111
146
|
const segments = change.path.split("/").filter(Boolean);
|
|
112
|
-
// Expected: ["service","protocols","websocket","channels",
|
|
147
|
+
// Expected base: ["service","protocols","websocket","channels", channelIndex, ...]
|
|
113
148
|
if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "websocket" || segments[3] !== "channels") {
|
|
114
149
|
return change;
|
|
115
150
|
}
|
|
116
|
-
const
|
|
151
|
+
const channelIndex = segments[4];
|
|
117
152
|
const next = segments[5];
|
|
118
153
|
const annotated = {
|
|
119
154
|
...change,
|
|
120
155
|
protocol: "websocket",
|
|
121
156
|
};
|
|
122
|
-
if (
|
|
157
|
+
if (typeof channelIndex === "undefined") {
|
|
123
158
|
return annotated;
|
|
124
159
|
}
|
|
125
|
-
// Channel-level changes
|
|
160
|
+
// Channel-level changes: /service/protocols/websocket/channels/{index}
|
|
126
161
|
if (!next) {
|
|
127
|
-
if (change.description === "Field removed") {
|
|
162
|
+
if (change.description === "Item removed" || change.description === "Field removed") {
|
|
128
163
|
annotated.kind = "websocket.channel.removed";
|
|
129
164
|
annotated.severity = "breaking";
|
|
130
165
|
}
|
|
131
|
-
else if (change.description === "Field added") {
|
|
166
|
+
else if (change.description === "Item added" || change.description === "Field added") {
|
|
132
167
|
annotated.kind = "websocket.channel.added";
|
|
133
168
|
annotated.severity = "non-breaking";
|
|
134
169
|
}
|
|
135
170
|
return annotated;
|
|
136
171
|
}
|
|
137
|
-
// Message-level changes
|
|
172
|
+
// Message-level changes: /service/protocols/websocket/channels/{index}/messages/{msgIndex}
|
|
138
173
|
if (next === "messages") {
|
|
139
|
-
const
|
|
140
|
-
if (typeof
|
|
174
|
+
const messageIndex = segments[6];
|
|
175
|
+
if (typeof messageIndex === "undefined") {
|
|
141
176
|
return annotated;
|
|
142
177
|
}
|
|
143
178
|
if (change.description === "Item removed") {
|
|
@@ -152,28 +187,31 @@ function annotateWebSocketChange(change) {
|
|
|
152
187
|
return annotated;
|
|
153
188
|
}
|
|
154
189
|
function annotateGraphQLChange(change) {
|
|
155
|
-
if (!change.path.startsWith("/service/protocols/graphql/
|
|
190
|
+
if (!change.path.startsWith("/service/protocols/graphql/")) {
|
|
156
191
|
return change;
|
|
157
192
|
}
|
|
158
193
|
const segments = change.path.split("/").filter(Boolean);
|
|
159
|
-
// Expected: ["service","protocols","graphql",
|
|
160
|
-
if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "graphql"
|
|
194
|
+
// Expected: ["service","protocols","graphql", kind, index, ...]
|
|
195
|
+
if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "graphql") {
|
|
196
|
+
return change;
|
|
197
|
+
}
|
|
198
|
+
const opKind = segments[3];
|
|
199
|
+
const index = segments[4];
|
|
200
|
+
if (!opKind || typeof index === "undefined") {
|
|
161
201
|
return change;
|
|
162
202
|
}
|
|
163
|
-
|
|
164
|
-
const opName = segments[5];
|
|
165
|
-
if (!opKind || !opName) {
|
|
203
|
+
if (opKind !== "queries" && opKind !== "mutations" && opKind !== "subscriptions") {
|
|
166
204
|
return change;
|
|
167
205
|
}
|
|
168
206
|
const annotated = {
|
|
169
207
|
...change,
|
|
170
208
|
protocol: "graphql",
|
|
171
209
|
};
|
|
172
|
-
if (change.description === "Field removed") {
|
|
210
|
+
if (change.description === "Item removed" || change.description === "Field removed") {
|
|
173
211
|
annotated.kind = "graphql.operation.removed";
|
|
174
212
|
annotated.severity = "breaking";
|
|
175
213
|
}
|
|
176
|
-
else if (change.description === "Field added") {
|
|
214
|
+
else if (change.description === "Item added" || change.description === "Field added") {
|
|
177
215
|
annotated.kind = "graphql.operation.added";
|
|
178
216
|
annotated.severity = "non-breaking";
|
|
179
217
|
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types/index.js"), exports);
|
|
18
|
+
__exportStar(require("./loader/index.js"), exports);
|
|
19
|
+
__exportStar(require("./validator/index.js"), exports);
|
|
20
|
+
__exportStar(require("./normalizer/index.js"), exports);
|
|
21
|
+
__exportStar(require("./diff/index.js"), exports);
|
|
22
|
+
__exportStar(require("./converters/index.js"), exports);
|
package/dist/normalizer/index.js
CHANGED
|
@@ -15,37 +15,22 @@ function normalizeValue(value) {
|
|
|
15
15
|
}
|
|
16
16
|
return value;
|
|
17
17
|
}
|
|
18
|
-
function
|
|
18
|
+
function normalizeRestRoutes(doc) {
|
|
19
19
|
if (!doc || !doc.service || !doc.service.protocols) {
|
|
20
20
|
return doc;
|
|
21
21
|
}
|
|
22
22
|
const protocols = doc.service.protocols;
|
|
23
23
|
const rest = protocols.rest;
|
|
24
|
-
if (!rest || !
|
|
24
|
+
if (!rest || !Array.isArray(rest.routes)) {
|
|
25
25
|
return doc;
|
|
26
26
|
}
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
const pathItemObj = pathItem;
|
|
36
|
-
const ordered = {};
|
|
37
|
-
for (const method of httpMethodOrder) {
|
|
38
|
-
if (Object.prototype.hasOwnProperty.call(pathItemObj, method)) {
|
|
39
|
-
ordered[method] = pathItemObj[method];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const remainingKeys = Object.keys(pathItemObj).filter((k) => !httpMethodOrder.includes(k)).sort();
|
|
43
|
-
for (const key of remainingKeys) {
|
|
44
|
-
ordered[key] = pathItemObj[key];
|
|
45
|
-
}
|
|
46
|
-
normalizedPaths[path] = ordered;
|
|
47
|
-
}
|
|
48
|
-
rest.paths = normalizedPaths;
|
|
27
|
+
const routes = [...rest.routes];
|
|
28
|
+
routes.sort((a, b) => {
|
|
29
|
+
const keyA = a.name || `${a.path} ${a.method}`;
|
|
30
|
+
const keyB = b.name || `${b.path} ${b.method}`;
|
|
31
|
+
return keyA.localeCompare(keyB);
|
|
32
|
+
});
|
|
33
|
+
rest.routes = routes;
|
|
49
34
|
return doc;
|
|
50
35
|
}
|
|
51
36
|
function normalizeWebSocket(doc) {
|
|
@@ -54,30 +39,29 @@ function normalizeWebSocket(doc) {
|
|
|
54
39
|
}
|
|
55
40
|
const protocols = doc.service.protocols;
|
|
56
41
|
const websocket = protocols.websocket;
|
|
57
|
-
if (!websocket || !
|
|
42
|
+
if (!websocket || !Array.isArray(websocket.channels)) {
|
|
58
43
|
return doc;
|
|
59
44
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const channel = originalChannels[name];
|
|
64
|
-
if (!channel) {
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
let messages = channel.messages;
|
|
68
|
-
if (Array.isArray(messages)) {
|
|
69
|
-
messages = [...messages].sort((a, b) => {
|
|
70
|
-
const aName = a?.name ?? "";
|
|
71
|
-
const bName = b?.name ?? "";
|
|
72
|
-
return aName.localeCompare(bName);
|
|
73
|
-
});
|
|
45
|
+
const channels = websocket.channels.map((channel) => {
|
|
46
|
+
if (!channel || !Array.isArray(channel.messages)) {
|
|
47
|
+
return channel;
|
|
74
48
|
}
|
|
75
|
-
|
|
49
|
+
const sortedMessages = [...channel.messages].sort((a, b) => {
|
|
50
|
+
const aName = a?.name ?? "";
|
|
51
|
+
const bName = b?.name ?? "";
|
|
52
|
+
return aName.localeCompare(bName);
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
76
55
|
...channel,
|
|
77
|
-
|
|
56
|
+
messages: sortedMessages,
|
|
78
57
|
};
|
|
79
|
-
}
|
|
80
|
-
|
|
58
|
+
});
|
|
59
|
+
channels.sort((a, b) => {
|
|
60
|
+
const aName = a?.name ?? "";
|
|
61
|
+
const bName = b?.name ?? "";
|
|
62
|
+
return aName.localeCompare(bName);
|
|
63
|
+
});
|
|
64
|
+
websocket.channels = channels;
|
|
81
65
|
return doc;
|
|
82
66
|
}
|
|
83
67
|
function normalizeGraphqlOperations(doc) {
|
|
@@ -86,20 +70,24 @@ function normalizeGraphqlOperations(doc) {
|
|
|
86
70
|
}
|
|
87
71
|
const protocols = doc.service.protocols;
|
|
88
72
|
const graphql = protocols.graphql;
|
|
89
|
-
if (!graphql
|
|
73
|
+
if (!graphql) {
|
|
90
74
|
return doc;
|
|
91
75
|
}
|
|
92
|
-
const kinds = [
|
|
76
|
+
const kinds = [
|
|
77
|
+
"queries",
|
|
78
|
+
"mutations",
|
|
79
|
+
"subscriptions",
|
|
80
|
+
];
|
|
93
81
|
for (const kind of kinds) {
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
82
|
+
const ops = graphql[kind];
|
|
83
|
+
if (!Array.isArray(ops)) {
|
|
96
84
|
continue;
|
|
97
85
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
86
|
+
graphql[kind] = [...ops].sort((a, b) => {
|
|
87
|
+
const aName = a?.name ?? "";
|
|
88
|
+
const bName = b?.name ?? "";
|
|
89
|
+
return aName.localeCompare(bName);
|
|
90
|
+
});
|
|
103
91
|
}
|
|
104
92
|
return doc;
|
|
105
93
|
}
|
|
@@ -112,5 +100,5 @@ function normalizeGraphqlOperations(doc) {
|
|
|
112
100
|
*/
|
|
113
101
|
export function normalizeUniSpec(doc, _options = {}) {
|
|
114
102
|
const normalized = normalizeValue(doc);
|
|
115
|
-
return normalizeWebSocket(normalizeGraphqlOperations(
|
|
103
|
+
return normalizeWebSocket(normalizeGraphqlOperations(normalizeRestRoutes(normalized)));
|
|
116
104
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,37 +1,87 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
mutations?: Record<string, unknown>;
|
|
7
|
-
subscriptions?: Record<string, unknown>;
|
|
1
|
+
export interface UniSpecGraphQLOperation {
|
|
2
|
+
name: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
deprecated?: boolean;
|
|
5
|
+
deprecationReason?: string;
|
|
8
6
|
}
|
|
9
7
|
export interface UniSpecGraphQLProtocol {
|
|
10
|
-
schema?:
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
schema?: string;
|
|
9
|
+
queries?: UniSpecGraphQLOperation[];
|
|
10
|
+
mutations?: UniSpecGraphQLOperation[];
|
|
11
|
+
subscriptions?: UniSpecGraphQLOperation[];
|
|
12
|
+
}
|
|
13
|
+
export interface UniSpecSecurityRequirement extends Array<string> {
|
|
13
14
|
}
|
|
14
15
|
export interface UniSpecWebSocketMessage {
|
|
15
|
-
name
|
|
16
|
-
summary?: string;
|
|
16
|
+
name: string;
|
|
17
17
|
description?: string;
|
|
18
|
-
|
|
19
|
-
extensions?: Record<string, unknown>;
|
|
18
|
+
schemaRef?: string;
|
|
20
19
|
}
|
|
21
20
|
export interface UniSpecWebSocketChannel {
|
|
22
|
-
|
|
23
|
-
title?: string;
|
|
21
|
+
name: string;
|
|
24
22
|
description?: string;
|
|
25
|
-
direction?:
|
|
23
|
+
direction?: "publish" | "subscribe" | "both";
|
|
26
24
|
messages?: UniSpecWebSocketMessage[];
|
|
25
|
+
security?: UniSpecSecurityRequirement[];
|
|
27
26
|
extensions?: Record<string, unknown>;
|
|
28
27
|
}
|
|
29
28
|
export interface UniSpecWebSocketProtocol {
|
|
30
|
-
channels?:
|
|
31
|
-
|
|
29
|
+
channels?: UniSpecWebSocketChannel[];
|
|
30
|
+
securitySchemes?: Record<string, Record<string, unknown>>;
|
|
31
|
+
}
|
|
32
|
+
export interface UniSpecRestParameter {
|
|
33
|
+
name: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
required?: boolean;
|
|
36
|
+
schemaRef?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface UniSpecRestMediaType {
|
|
39
|
+
schemaRef?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface UniSpecRestContent {
|
|
42
|
+
[mediaType: string]: UniSpecRestMediaType;
|
|
43
|
+
}
|
|
44
|
+
export interface UniSpecRestRequestBody {
|
|
45
|
+
description?: string;
|
|
46
|
+
required?: boolean;
|
|
47
|
+
content?: UniSpecRestContent;
|
|
48
|
+
}
|
|
49
|
+
export interface UniSpecRestResponse {
|
|
50
|
+
description?: string;
|
|
51
|
+
content?: UniSpecRestContent;
|
|
52
|
+
}
|
|
53
|
+
export interface UniSpecRestRoute {
|
|
54
|
+
name?: string;
|
|
55
|
+
summary?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
path: string;
|
|
58
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
59
|
+
pathParams?: UniSpecRestParameter[];
|
|
60
|
+
queryParams?: UniSpecRestParameter[];
|
|
61
|
+
headers?: UniSpecRestParameter[];
|
|
62
|
+
requestBody?: UniSpecRestRequestBody;
|
|
63
|
+
responses?: Record<string, UniSpecRestResponse>;
|
|
64
|
+
security?: UniSpecSecurityRequirement[];
|
|
65
|
+
}
|
|
66
|
+
export interface UniSpecRestProtocol {
|
|
67
|
+
routes?: UniSpecRestRoute[];
|
|
68
|
+
securitySchemes?: Record<string, Record<string, unknown>>;
|
|
69
|
+
}
|
|
70
|
+
export interface UniSpecSchemaDefinition {
|
|
71
|
+
jsonSchema: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
export interface UniSpecSchemas {
|
|
74
|
+
[name: string]: UniSpecSchemaDefinition;
|
|
75
|
+
}
|
|
76
|
+
export interface UniSpecEnvironment {
|
|
77
|
+
name: string;
|
|
78
|
+
baseUrl: string;
|
|
79
|
+
region?: string;
|
|
80
|
+
labels?: Record<string, string>;
|
|
81
|
+
isDefault?: boolean;
|
|
32
82
|
}
|
|
33
83
|
export interface UniSpecServiceProtocols {
|
|
34
|
-
rest?:
|
|
84
|
+
rest?: UniSpecRestProtocol;
|
|
35
85
|
graphql?: UniSpecGraphQLProtocol;
|
|
36
86
|
websocket?: UniSpecWebSocketProtocol;
|
|
37
87
|
}
|
|
@@ -39,7 +89,13 @@ export interface UniSpecService {
|
|
|
39
89
|
name: string;
|
|
40
90
|
title?: string;
|
|
41
91
|
description?: string;
|
|
92
|
+
version?: string;
|
|
93
|
+
owner?: string;
|
|
94
|
+
tags?: string[];
|
|
95
|
+
links?: Record<string, string>;
|
|
42
96
|
protocols?: UniSpecServiceProtocols;
|
|
97
|
+
schemas?: UniSpecSchemas;
|
|
98
|
+
environments?: UniSpecEnvironment[];
|
|
43
99
|
}
|
|
44
100
|
export interface UniSpecDocument {
|
|
45
101
|
unispecVersion: string;
|
|
@@ -55,3 +111,88 @@ export interface ValidationResult {
|
|
|
55
111
|
valid: boolean;
|
|
56
112
|
errors: ValidationError[];
|
|
57
113
|
}
|
|
114
|
+
export type UniSpecTestProtocol = "rest" | "graphql" | "websocket";
|
|
115
|
+
export interface UniSpecTestTarget {
|
|
116
|
+
protocol: UniSpecTestProtocol;
|
|
117
|
+
operationId: string;
|
|
118
|
+
environment?: string;
|
|
119
|
+
}
|
|
120
|
+
export interface UniSpecRestTestParams {
|
|
121
|
+
path?: Record<string, unknown>;
|
|
122
|
+
query?: Record<string, unknown>;
|
|
123
|
+
}
|
|
124
|
+
export interface UniSpecRestTestRequest {
|
|
125
|
+
params?: UniSpecRestTestParams;
|
|
126
|
+
headers?: Record<string, unknown>;
|
|
127
|
+
body?: unknown;
|
|
128
|
+
authProfile?: string;
|
|
129
|
+
}
|
|
130
|
+
export type UniSpecRestExpectedStatus = number | number[];
|
|
131
|
+
export type UniSpecRestBodyExpectationMode = "exact" | "contains" | "schemaOnly" | "snapshot";
|
|
132
|
+
export interface UniSpecRestBodyExpectation {
|
|
133
|
+
mode?: UniSpecRestBodyExpectationMode;
|
|
134
|
+
json?: unknown;
|
|
135
|
+
schemaRef?: string;
|
|
136
|
+
}
|
|
137
|
+
export interface UniSpecRestTestExpect {
|
|
138
|
+
status: UniSpecRestExpectedStatus;
|
|
139
|
+
headers?: Record<string, unknown>;
|
|
140
|
+
body?: UniSpecRestBodyExpectation;
|
|
141
|
+
}
|
|
142
|
+
export interface UniSpecGraphQLTestRequest {
|
|
143
|
+
operationName?: string;
|
|
144
|
+
query: string;
|
|
145
|
+
variables?: Record<string, unknown>;
|
|
146
|
+
headers?: Record<string, unknown>;
|
|
147
|
+
authProfile?: string;
|
|
148
|
+
}
|
|
149
|
+
export interface UniSpecGraphQLTestExpect {
|
|
150
|
+
data?: unknown;
|
|
151
|
+
errors?: unknown;
|
|
152
|
+
bodyMode?: string;
|
|
153
|
+
}
|
|
154
|
+
export type UniSpecWebSocketTestMessageActionType = "send" | "expect";
|
|
155
|
+
export interface UniSpecWebSocketTestMessageAction {
|
|
156
|
+
type: UniSpecWebSocketTestMessageActionType;
|
|
157
|
+
messageName: string;
|
|
158
|
+
payload?: unknown;
|
|
159
|
+
}
|
|
160
|
+
export interface UniSpecWebSocketTestRequest {
|
|
161
|
+
channel: string;
|
|
162
|
+
direction?: "publish" | "subscribe" | "both";
|
|
163
|
+
messages?: UniSpecWebSocketTestMessageAction[];
|
|
164
|
+
authProfile?: string;
|
|
165
|
+
}
|
|
166
|
+
export interface UniSpecWebSocketTestExpect {
|
|
167
|
+
messages?: UniSpecWebSocketTestMessageAction[];
|
|
168
|
+
timeoutMs?: number;
|
|
169
|
+
}
|
|
170
|
+
export interface UniSpecTestRequest {
|
|
171
|
+
rest?: UniSpecRestTestRequest;
|
|
172
|
+
graphql?: UniSpecGraphQLTestRequest;
|
|
173
|
+
websocket?: UniSpecWebSocketTestRequest;
|
|
174
|
+
}
|
|
175
|
+
export interface UniSpecTestExpect {
|
|
176
|
+
rest?: UniSpecRestTestExpect;
|
|
177
|
+
graphql?: UniSpecGraphQLTestExpect;
|
|
178
|
+
websocket?: UniSpecWebSocketTestExpect;
|
|
179
|
+
}
|
|
180
|
+
export interface UniSpecTestCase {
|
|
181
|
+
name: string;
|
|
182
|
+
description?: string;
|
|
183
|
+
target: UniSpecTestTarget;
|
|
184
|
+
request: UniSpecTestRequest;
|
|
185
|
+
expect: UniSpecTestExpect;
|
|
186
|
+
tags?: string[];
|
|
187
|
+
extensions?: Record<string, unknown>;
|
|
188
|
+
}
|
|
189
|
+
export interface UniSpecTestsDocument {
|
|
190
|
+
uniSpecTestsVersion: string;
|
|
191
|
+
target: {
|
|
192
|
+
serviceName: string;
|
|
193
|
+
serviceVersion?: string;
|
|
194
|
+
environment?: string;
|
|
195
|
+
};
|
|
196
|
+
tests: UniSpecTestCase[];
|
|
197
|
+
extensions?: Record<string, unknown>;
|
|
198
|
+
}
|
package/dist/types/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { UniSpecDocument, ValidationResult } from "../types";
|
|
1
|
+
import { UniSpecDocument, UniSpecTestsDocument, ValidationResult } from "../types";
|
|
2
2
|
export interface ValidateOptions {
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
5
|
* Validate a UniSpec document against the UniSpec JSON Schema.
|
|
6
6
|
*/
|
|
7
7
|
export declare function validateUniSpec(doc: UniSpecDocument, _options?: ValidateOptions): Promise<ValidationResult>;
|
|
8
|
+
/**
|
|
9
|
+
* Validate a UniSpec Tests document against the UniSpec Tests JSON Schema.
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateUniSpecTests(doc: UniSpecTestsDocument, _options?: ValidateOptions): Promise<ValidationResult>;
|
package/dist/validator/index.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import Ajv2020 from "ajv/dist/2020.js";
|
|
2
|
-
import
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
3
4
|
import { unispec as unispecSchema, manifest as unispecManifest } from "@unispechq/unispec-schema";
|
|
4
|
-
const require = createRequire(import.meta.url);
|
|
5
5
|
const ajv = new Ajv2020({
|
|
6
6
|
allErrors: true,
|
|
7
7
|
strict: true,
|
|
8
8
|
});
|
|
9
|
+
// Register minimal URI format to satisfy UniSpec schemas (service.environments[*].baseUrl)
|
|
10
|
+
ajv.addFormat("uri", true);
|
|
9
11
|
// Register all UniSpec subschemas so that Ajv can resolve internal $ref links
|
|
10
12
|
try {
|
|
11
|
-
const
|
|
12
|
-
const schemaDir = schemaRootPath.replace(/index\.(cjs|mjs|js)$/u, "schema/");
|
|
13
|
+
const schemaDir = path.join(process.cwd(), "node_modules", "@unispechq", "unispec-schema", "schema");
|
|
13
14
|
const types = unispecManifest?.types ?? {};
|
|
14
15
|
const typeSchemaPaths = Object.values(types).map((rel) => String(rel));
|
|
15
|
-
const loadedTypeSchemas = typeSchemaPaths
|
|
16
|
+
const loadedTypeSchemas = typeSchemaPaths
|
|
17
|
+
.map((relPath) => path.join(schemaDir, relPath))
|
|
18
|
+
.filter((filePath) => fs.existsSync(filePath))
|
|
19
|
+
.map((filePath) => JSON.parse(fs.readFileSync(filePath, "utf8")));
|
|
16
20
|
ajv.addSchema(loadedTypeSchemas);
|
|
17
21
|
}
|
|
18
22
|
catch {
|
|
@@ -20,6 +24,7 @@ catch {
|
|
|
20
24
|
// parts of the schema that do not rely on those $ref references.
|
|
21
25
|
}
|
|
22
26
|
const validateFn = ajv.compile(unispecSchema);
|
|
27
|
+
let validateTestsFn;
|
|
23
28
|
function mapAjvErrors(errors) {
|
|
24
29
|
if (!errors)
|
|
25
30
|
return [];
|
|
@@ -45,3 +50,25 @@ export async function validateUniSpec(doc, _options = {}) {
|
|
|
45
50
|
errors: mapAjvErrors(validateFn.errors),
|
|
46
51
|
};
|
|
47
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate a UniSpec Tests document against the UniSpec Tests JSON Schema.
|
|
55
|
+
*/
|
|
56
|
+
export async function validateUniSpecTests(doc, _options = {}) {
|
|
57
|
+
if (!validateTestsFn) {
|
|
58
|
+
const schemaDir = path.join(process.cwd(), "node_modules", "@unispechq", "unispec-schema", "schema");
|
|
59
|
+
const testsSchemaPath = path.join(schemaDir, "unispec-tests.schema.json");
|
|
60
|
+
const testsSchema = JSON.parse(fs.readFileSync(testsSchemaPath, "utf8"));
|
|
61
|
+
validateTestsFn = ajv.compile(testsSchema);
|
|
62
|
+
}
|
|
63
|
+
const valid = validateTestsFn(doc);
|
|
64
|
+
if (valid) {
|
|
65
|
+
return {
|
|
66
|
+
valid: true,
|
|
67
|
+
errors: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
valid: false,
|
|
72
|
+
errors: mapAjvErrors(validateTestsFn.errors),
|
|
73
|
+
};
|
|
74
|
+
}
|