@unispechq/unispec-core 0.2.2 → 0.2.4
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/dist/cjs/converters/index.js +13 -22
- package/dist/cjs/diff/index.js +22 -22
- package/dist/cjs/normalizer/index.js +6 -6
- package/dist/cjs/validator/index.js +57 -8
- package/dist/converters/index.js +13 -22
- package/dist/diff/index.js +22 -22
- package/dist/normalizer/index.js +6 -6
- package/dist/types/index.d.ts +1 -10
- package/dist/validator/index.js +57 -8
- package/package.json +22 -2
|
@@ -4,23 +4,16 @@ exports.toOpenAPI = toOpenAPI;
|
|
|
4
4
|
exports.toGraphQLSDL = toGraphQLSDL;
|
|
5
5
|
exports.toWebSocketModel = toWebSocketModel;
|
|
6
6
|
function toOpenAPI(doc) {
|
|
7
|
-
const
|
|
8
|
-
const rest = (service.protocols?.rest ?? {});
|
|
7
|
+
const rest = (doc.protocols?.rest ?? {});
|
|
9
8
|
const info = {
|
|
10
|
-
title:
|
|
11
|
-
description:
|
|
9
|
+
title: doc.name,
|
|
10
|
+
description: doc.description,
|
|
12
11
|
};
|
|
13
|
-
|
|
14
|
-
const servers = Array.isArray(service.environments)
|
|
15
|
-
? service.environments.map((env) => ({
|
|
16
|
-
url: env.baseUrl,
|
|
17
|
-
description: env.name,
|
|
18
|
-
}))
|
|
19
|
-
: [];
|
|
12
|
+
const servers = [];
|
|
20
13
|
const paths = {};
|
|
21
14
|
const components = {};
|
|
22
|
-
// Map
|
|
23
|
-
const schemas = (
|
|
15
|
+
// Map doc.schemas into OpenAPI components.schemas
|
|
16
|
+
const schemas = (doc.schemas ?? {});
|
|
24
17
|
const componentsSchemas = {};
|
|
25
18
|
for (const [name, def] of Object.entries(schemas)) {
|
|
26
19
|
componentsSchemas[name] = def.jsonSchema;
|
|
@@ -145,14 +138,13 @@ function toGraphQLSDL(doc) {
|
|
|
145
138
|
// via a Query field. This does not attempt to interpret the full GraphQL
|
|
146
139
|
// protocol structure yet, but provides a stable, deterministic SDL shape
|
|
147
140
|
// based on top-level UniSpec document fields.
|
|
148
|
-
const graphql = doc.
|
|
141
|
+
const graphql = doc.protocols?.graphql;
|
|
149
142
|
const customSDL = graphql?.schema;
|
|
150
143
|
if (typeof customSDL === "string" && customSDL.trim()) {
|
|
151
144
|
return { sdl: customSDL };
|
|
152
145
|
}
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const description = service.description ?? "";
|
|
146
|
+
const title = doc.name;
|
|
147
|
+
const description = doc.description ?? "";
|
|
156
148
|
const lines = [];
|
|
157
149
|
if (title || description) {
|
|
158
150
|
lines.push("\"\"");
|
|
@@ -180,8 +172,7 @@ function toWebSocketModel(doc) {
|
|
|
180
172
|
// It exposes service metadata, a normalized list of channels and the raw
|
|
181
173
|
// websocket protocol object, while also embedding the original UniSpec
|
|
182
174
|
// document under a technical key for debugging and introspection.
|
|
183
|
-
const
|
|
184
|
-
const websocket = (service.protocols?.websocket ?? {});
|
|
175
|
+
const websocket = (doc.protocols?.websocket ?? {});
|
|
185
176
|
const channelsArray = Array.isArray(websocket.channels) ? websocket.channels : [];
|
|
186
177
|
const channels = [...channelsArray]
|
|
187
178
|
.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""))
|
|
@@ -195,9 +186,9 @@ function toWebSocketModel(doc) {
|
|
|
195
186
|
}));
|
|
196
187
|
return {
|
|
197
188
|
service: {
|
|
198
|
-
name:
|
|
199
|
-
title:
|
|
200
|
-
description:
|
|
189
|
+
name: doc.name,
|
|
190
|
+
title: undefined,
|
|
191
|
+
description: doc.description,
|
|
201
192
|
},
|
|
202
193
|
channels,
|
|
203
194
|
rawProtocol: websocket,
|
package/dist/cjs/diff/index.js
CHANGED
|
@@ -40,11 +40,11 @@ function diffValues(oldVal, newVal, basePath, out) {
|
|
|
40
40
|
// Arrays
|
|
41
41
|
if (Array.isArray(oldVal) && Array.isArray(newVal)) {
|
|
42
42
|
// Special handling for UniSpec collections identified by "name"
|
|
43
|
-
const isNamedCollection = basePath === "/
|
|
44
|
-
basePath === "/
|
|
45
|
-
basePath === "/
|
|
46
|
-
basePath === "/
|
|
47
|
-
basePath === "/
|
|
43
|
+
const isNamedCollection = basePath === "/protocols/rest/routes" ||
|
|
44
|
+
basePath === "/protocols/websocket/channels" ||
|
|
45
|
+
basePath === "/protocols/graphql/queries" ||
|
|
46
|
+
basePath === "/protocols/graphql/mutations" ||
|
|
47
|
+
basePath === "/protocols/graphql/subscriptions";
|
|
48
48
|
if (isNamedCollection) {
|
|
49
49
|
const oldByName = new Map();
|
|
50
50
|
const newByName = new Map();
|
|
@@ -116,15 +116,15 @@ function diffValues(oldVal, newVal, basePath, out) {
|
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
118
|
function annotateRestChange(change) {
|
|
119
|
-
if (!change.path.startsWith("/
|
|
119
|
+
if (!change.path.startsWith("/protocols/rest/routes/")) {
|
|
120
120
|
return change;
|
|
121
121
|
}
|
|
122
122
|
const segments = change.path.split("/").filter(Boolean);
|
|
123
|
-
// Expected shape: ["
|
|
124
|
-
if (segments[0] !== "
|
|
123
|
+
// Expected shape: ["protocols", "rest", "routes", index]
|
|
124
|
+
if (segments[0] !== "protocols" || segments[1] !== "rest" || segments[2] !== "routes") {
|
|
125
125
|
return change;
|
|
126
126
|
}
|
|
127
|
-
const index = segments[
|
|
127
|
+
const index = segments[3];
|
|
128
128
|
if (typeof index === "undefined") {
|
|
129
129
|
return change;
|
|
130
130
|
}
|
|
@@ -143,16 +143,16 @@ function annotateRestChange(change) {
|
|
|
143
143
|
return annotated;
|
|
144
144
|
}
|
|
145
145
|
function annotateWebSocketChange(change) {
|
|
146
|
-
if (!change.path.startsWith("/
|
|
146
|
+
if (!change.path.startsWith("/protocols/websocket/channels/")) {
|
|
147
147
|
return change;
|
|
148
148
|
}
|
|
149
149
|
const segments = change.path.split("/").filter(Boolean);
|
|
150
|
-
// Expected base: ["
|
|
151
|
-
if (segments[0] !== "
|
|
150
|
+
// Expected base: ["protocols","websocket","channels", channelIndex, ...]
|
|
151
|
+
if (segments[0] !== "protocols" || segments[1] !== "websocket" || segments[2] !== "channels") {
|
|
152
152
|
return change;
|
|
153
153
|
}
|
|
154
|
-
const channelIndex = segments[
|
|
155
|
-
const next = segments[
|
|
154
|
+
const channelIndex = segments[3];
|
|
155
|
+
const next = segments[4];
|
|
156
156
|
const annotated = {
|
|
157
157
|
...change,
|
|
158
158
|
protocol: "websocket",
|
|
@@ -160,7 +160,7 @@ function annotateWebSocketChange(change) {
|
|
|
160
160
|
if (typeof channelIndex === "undefined") {
|
|
161
161
|
return annotated;
|
|
162
162
|
}
|
|
163
|
-
// Channel-level changes: /
|
|
163
|
+
// Channel-level changes: /protocols/websocket/channels/{index}
|
|
164
164
|
if (!next) {
|
|
165
165
|
if (change.description === "Item removed" || change.description === "Field removed") {
|
|
166
166
|
annotated.kind = "websocket.channel.removed";
|
|
@@ -172,9 +172,9 @@ function annotateWebSocketChange(change) {
|
|
|
172
172
|
}
|
|
173
173
|
return annotated;
|
|
174
174
|
}
|
|
175
|
-
// Message-level changes: /
|
|
175
|
+
// Message-level changes: /protocols/websocket/channels/{index}/messages/{msgIndex}
|
|
176
176
|
if (next === "messages") {
|
|
177
|
-
const messageIndex = segments[
|
|
177
|
+
const messageIndex = segments[5];
|
|
178
178
|
if (typeof messageIndex === "undefined") {
|
|
179
179
|
return annotated;
|
|
180
180
|
}
|
|
@@ -190,16 +190,16 @@ function annotateWebSocketChange(change) {
|
|
|
190
190
|
return annotated;
|
|
191
191
|
}
|
|
192
192
|
function annotateGraphQLChange(change) {
|
|
193
|
-
if (!change.path.startsWith("/
|
|
193
|
+
if (!change.path.startsWith("/protocols/graphql/")) {
|
|
194
194
|
return change;
|
|
195
195
|
}
|
|
196
196
|
const segments = change.path.split("/").filter(Boolean);
|
|
197
|
-
// Expected: ["
|
|
198
|
-
if (segments[0] !== "
|
|
197
|
+
// Expected: ["protocols","graphql", kind, index, ...]
|
|
198
|
+
if (segments[0] !== "protocols" || segments[1] !== "graphql") {
|
|
199
199
|
return change;
|
|
200
200
|
}
|
|
201
|
-
const opKind = segments[
|
|
202
|
-
const index = segments[
|
|
201
|
+
const opKind = segments[2];
|
|
202
|
+
const index = segments[3];
|
|
203
203
|
if (!opKind || typeof index === "undefined") {
|
|
204
204
|
return change;
|
|
205
205
|
}
|
|
@@ -19,10 +19,10 @@ function normalizeValue(value) {
|
|
|
19
19
|
return value;
|
|
20
20
|
}
|
|
21
21
|
function normalizeRestRoutes(doc) {
|
|
22
|
-
if (!doc || !doc.
|
|
22
|
+
if (!doc || !doc.protocols) {
|
|
23
23
|
return doc;
|
|
24
24
|
}
|
|
25
|
-
const protocols = doc.
|
|
25
|
+
const protocols = doc.protocols;
|
|
26
26
|
const rest = protocols.rest;
|
|
27
27
|
if (!rest || !Array.isArray(rest.routes)) {
|
|
28
28
|
return doc;
|
|
@@ -37,10 +37,10 @@ function normalizeRestRoutes(doc) {
|
|
|
37
37
|
return doc;
|
|
38
38
|
}
|
|
39
39
|
function normalizeWebSocket(doc) {
|
|
40
|
-
if (!doc || !doc.
|
|
40
|
+
if (!doc || !doc.protocols) {
|
|
41
41
|
return doc;
|
|
42
42
|
}
|
|
43
|
-
const protocols = doc.
|
|
43
|
+
const protocols = doc.protocols;
|
|
44
44
|
const websocket = protocols.websocket;
|
|
45
45
|
if (!websocket || !Array.isArray(websocket.channels)) {
|
|
46
46
|
return doc;
|
|
@@ -68,10 +68,10 @@ function normalizeWebSocket(doc) {
|
|
|
68
68
|
return doc;
|
|
69
69
|
}
|
|
70
70
|
function normalizeGraphqlOperations(doc) {
|
|
71
|
-
if (!doc || !doc.
|
|
71
|
+
if (!doc || !doc.protocols) {
|
|
72
72
|
return doc;
|
|
73
73
|
}
|
|
74
|
-
const protocols = doc.
|
|
74
|
+
const protocols = doc.protocols;
|
|
75
75
|
const graphql = protocols.graphql;
|
|
76
76
|
if (!graphql) {
|
|
77
77
|
return doc;
|
|
@@ -8,6 +8,7 @@ exports.validateUniSpecTests = validateUniSpecTests;
|
|
|
8
8
|
const _2020_js_1 = __importDefault(require("ajv/dist/2020.js"));
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const module_1 = require("module");
|
|
11
12
|
const unispec_schema_1 = require("@unispechq/unispec-schema");
|
|
12
13
|
const ajv = new _2020_js_1.default({
|
|
13
14
|
allErrors: true,
|
|
@@ -15,16 +16,53 @@ const ajv = new _2020_js_1.default({
|
|
|
15
16
|
});
|
|
16
17
|
// Register minimal URI format to satisfy UniSpec schemas (service.environments[*].baseUrl)
|
|
17
18
|
ajv.addFormat("uri", true);
|
|
19
|
+
function findPackageRoot(startFilePath) {
|
|
20
|
+
let dir = path_1.default.dirname(startFilePath);
|
|
21
|
+
for (let i = 0; i < 50; i++) {
|
|
22
|
+
const pkgJsonPath = path_1.default.join(dir, "package.json");
|
|
23
|
+
if (fs_1.default.existsSync(pkgJsonPath)) {
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
const parent = path_1.default.dirname(dir);
|
|
27
|
+
if (parent === dir) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Cannot locate package.json for resolved path: ${startFilePath}`);
|
|
33
|
+
}
|
|
34
|
+
function getUniSpecSchemaDir() {
|
|
35
|
+
const localRequire = typeof require !== "undefined" ? require : (0, module_1.createRequire)(path_1.default.join(process.cwd(), "package.json"));
|
|
36
|
+
try {
|
|
37
|
+
const pkgJsonPath = localRequire.resolve("@unispechq/unispec-schema/package.json");
|
|
38
|
+
return path_1.default.join(path_1.default.dirname(pkgJsonPath), "schema");
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
const entryPath = localRequire.resolve("@unispechq/unispec-schema");
|
|
42
|
+
const pkgRoot = findPackageRoot(entryPath);
|
|
43
|
+
return path_1.default.join(pkgRoot, "schema");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
18
46
|
// Register all UniSpec subschemas so that Ajv can resolve internal $ref links
|
|
19
47
|
try {
|
|
20
|
-
const schemaDir =
|
|
48
|
+
const schemaDir = getUniSpecSchemaDir();
|
|
21
49
|
const types = unispec_schema_1.manifest?.types ?? {};
|
|
22
50
|
const typeSchemaPaths = Object.values(types).map((rel) => String(rel));
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
51
|
+
for (const relPath of typeSchemaPaths) {
|
|
52
|
+
try {
|
|
53
|
+
const filePath = path_1.default.join(schemaDir, relPath);
|
|
54
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const schema = JSON.parse(fs_1.default.readFileSync(filePath, "utf8"));
|
|
58
|
+
const normalizedRelPath = String(relPath).replace(/^\.\//, "");
|
|
59
|
+
const fallbackId = `https://unispec.dev/schema/${normalizedRelPath}`;
|
|
60
|
+
ajv.addSchema(schema, fallbackId);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Ignore individual schema registration failures to keep the validator usable.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
28
66
|
}
|
|
29
67
|
catch {
|
|
30
68
|
// If subschemas cannot be loaded for some reason, validation will still work for
|
|
@@ -45,7 +83,18 @@ function mapAjvErrors(errors) {
|
|
|
45
83
|
* Validate a UniSpec document against the UniSpec JSON Schema.
|
|
46
84
|
*/
|
|
47
85
|
async function validateUniSpec(doc, _options = {}) {
|
|
48
|
-
const
|
|
86
|
+
const docForValidation = {
|
|
87
|
+
unispecVersion: "0.0.0",
|
|
88
|
+
service: {
|
|
89
|
+
name: doc.name,
|
|
90
|
+
description: doc.description,
|
|
91
|
+
version: doc.version,
|
|
92
|
+
protocols: doc.protocols,
|
|
93
|
+
schemas: doc.schemas,
|
|
94
|
+
},
|
|
95
|
+
extensions: doc.extensions,
|
|
96
|
+
};
|
|
97
|
+
const valid = validateFn(docForValidation);
|
|
49
98
|
if (valid) {
|
|
50
99
|
return {
|
|
51
100
|
valid: true,
|
|
@@ -62,7 +111,7 @@ async function validateUniSpec(doc, _options = {}) {
|
|
|
62
111
|
*/
|
|
63
112
|
async function validateUniSpecTests(doc, _options = {}) {
|
|
64
113
|
if (!validateTestsFn) {
|
|
65
|
-
const schemaDir =
|
|
114
|
+
const schemaDir = getUniSpecSchemaDir();
|
|
66
115
|
const testsSchemaPath = path_1.default.join(schemaDir, "unispec-tests.schema.json");
|
|
67
116
|
const testsSchema = JSON.parse(fs_1.default.readFileSync(testsSchemaPath, "utf8"));
|
|
68
117
|
validateTestsFn = ajv.compile(testsSchema);
|
package/dist/converters/index.js
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
export function toOpenAPI(doc) {
|
|
2
|
-
const
|
|
3
|
-
const rest = (service.protocols?.rest ?? {});
|
|
2
|
+
const rest = (doc.protocols?.rest ?? {});
|
|
4
3
|
const info = {
|
|
5
|
-
title:
|
|
6
|
-
description:
|
|
4
|
+
title: doc.name,
|
|
5
|
+
description: doc.description,
|
|
7
6
|
};
|
|
8
|
-
|
|
9
|
-
const servers = Array.isArray(service.environments)
|
|
10
|
-
? service.environments.map((env) => ({
|
|
11
|
-
url: env.baseUrl,
|
|
12
|
-
description: env.name,
|
|
13
|
-
}))
|
|
14
|
-
: [];
|
|
7
|
+
const servers = [];
|
|
15
8
|
const paths = {};
|
|
16
9
|
const components = {};
|
|
17
|
-
// Map
|
|
18
|
-
const schemas = (
|
|
10
|
+
// Map doc.schemas into OpenAPI components.schemas
|
|
11
|
+
const schemas = (doc.schemas ?? {});
|
|
19
12
|
const componentsSchemas = {};
|
|
20
13
|
for (const [name, def] of Object.entries(schemas)) {
|
|
21
14
|
componentsSchemas[name] = def.jsonSchema;
|
|
@@ -140,14 +133,13 @@ export function toGraphQLSDL(doc) {
|
|
|
140
133
|
// via a Query field. This does not attempt to interpret the full GraphQL
|
|
141
134
|
// protocol structure yet, but provides a stable, deterministic SDL shape
|
|
142
135
|
// based on top-level UniSpec document fields.
|
|
143
|
-
const graphql = doc.
|
|
136
|
+
const graphql = doc.protocols?.graphql;
|
|
144
137
|
const customSDL = graphql?.schema;
|
|
145
138
|
if (typeof customSDL === "string" && customSDL.trim()) {
|
|
146
139
|
return { sdl: customSDL };
|
|
147
140
|
}
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const description = service.description ?? "";
|
|
141
|
+
const title = doc.name;
|
|
142
|
+
const description = doc.description ?? "";
|
|
151
143
|
const lines = [];
|
|
152
144
|
if (title || description) {
|
|
153
145
|
lines.push("\"\"");
|
|
@@ -175,8 +167,7 @@ export function toWebSocketModel(doc) {
|
|
|
175
167
|
// It exposes service metadata, a normalized list of channels and the raw
|
|
176
168
|
// websocket protocol object, while also embedding the original UniSpec
|
|
177
169
|
// document under a technical key for debugging and introspection.
|
|
178
|
-
const
|
|
179
|
-
const websocket = (service.protocols?.websocket ?? {});
|
|
170
|
+
const websocket = (doc.protocols?.websocket ?? {});
|
|
180
171
|
const channelsArray = Array.isArray(websocket.channels) ? websocket.channels : [];
|
|
181
172
|
const channels = [...channelsArray]
|
|
182
173
|
.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""))
|
|
@@ -190,9 +181,9 @@ export function toWebSocketModel(doc) {
|
|
|
190
181
|
}));
|
|
191
182
|
return {
|
|
192
183
|
service: {
|
|
193
|
-
name:
|
|
194
|
-
title:
|
|
195
|
-
description:
|
|
184
|
+
name: doc.name,
|
|
185
|
+
title: undefined,
|
|
186
|
+
description: doc.description,
|
|
196
187
|
},
|
|
197
188
|
channels,
|
|
198
189
|
rawProtocol: websocket,
|
package/dist/diff/index.js
CHANGED
|
@@ -37,11 +37,11 @@ function diffValues(oldVal, newVal, basePath, out) {
|
|
|
37
37
|
// Arrays
|
|
38
38
|
if (Array.isArray(oldVal) && Array.isArray(newVal)) {
|
|
39
39
|
// Special handling for UniSpec collections identified by "name"
|
|
40
|
-
const isNamedCollection = basePath === "/
|
|
41
|
-
basePath === "/
|
|
42
|
-
basePath === "/
|
|
43
|
-
basePath === "/
|
|
44
|
-
basePath === "/
|
|
40
|
+
const isNamedCollection = basePath === "/protocols/rest/routes" ||
|
|
41
|
+
basePath === "/protocols/websocket/channels" ||
|
|
42
|
+
basePath === "/protocols/graphql/queries" ||
|
|
43
|
+
basePath === "/protocols/graphql/mutations" ||
|
|
44
|
+
basePath === "/protocols/graphql/subscriptions";
|
|
45
45
|
if (isNamedCollection) {
|
|
46
46
|
const oldByName = new Map();
|
|
47
47
|
const newByName = new Map();
|
|
@@ -113,15 +113,15 @@ function diffValues(oldVal, newVal, basePath, out) {
|
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
function annotateRestChange(change) {
|
|
116
|
-
if (!change.path.startsWith("/
|
|
116
|
+
if (!change.path.startsWith("/protocols/rest/routes/")) {
|
|
117
117
|
return change;
|
|
118
118
|
}
|
|
119
119
|
const segments = change.path.split("/").filter(Boolean);
|
|
120
|
-
// Expected shape: ["
|
|
121
|
-
if (segments[0] !== "
|
|
120
|
+
// Expected shape: ["protocols", "rest", "routes", index]
|
|
121
|
+
if (segments[0] !== "protocols" || segments[1] !== "rest" || segments[2] !== "routes") {
|
|
122
122
|
return change;
|
|
123
123
|
}
|
|
124
|
-
const index = segments[
|
|
124
|
+
const index = segments[3];
|
|
125
125
|
if (typeof index === "undefined") {
|
|
126
126
|
return change;
|
|
127
127
|
}
|
|
@@ -140,16 +140,16 @@ function annotateRestChange(change) {
|
|
|
140
140
|
return annotated;
|
|
141
141
|
}
|
|
142
142
|
function annotateWebSocketChange(change) {
|
|
143
|
-
if (!change.path.startsWith("/
|
|
143
|
+
if (!change.path.startsWith("/protocols/websocket/channels/")) {
|
|
144
144
|
return change;
|
|
145
145
|
}
|
|
146
146
|
const segments = change.path.split("/").filter(Boolean);
|
|
147
|
-
// Expected base: ["
|
|
148
|
-
if (segments[0] !== "
|
|
147
|
+
// Expected base: ["protocols","websocket","channels", channelIndex, ...]
|
|
148
|
+
if (segments[0] !== "protocols" || segments[1] !== "websocket" || segments[2] !== "channels") {
|
|
149
149
|
return change;
|
|
150
150
|
}
|
|
151
|
-
const channelIndex = segments[
|
|
152
|
-
const next = segments[
|
|
151
|
+
const channelIndex = segments[3];
|
|
152
|
+
const next = segments[4];
|
|
153
153
|
const annotated = {
|
|
154
154
|
...change,
|
|
155
155
|
protocol: "websocket",
|
|
@@ -157,7 +157,7 @@ function annotateWebSocketChange(change) {
|
|
|
157
157
|
if (typeof channelIndex === "undefined") {
|
|
158
158
|
return annotated;
|
|
159
159
|
}
|
|
160
|
-
// Channel-level changes: /
|
|
160
|
+
// Channel-level changes: /protocols/websocket/channels/{index}
|
|
161
161
|
if (!next) {
|
|
162
162
|
if (change.description === "Item removed" || change.description === "Field removed") {
|
|
163
163
|
annotated.kind = "websocket.channel.removed";
|
|
@@ -169,9 +169,9 @@ function annotateWebSocketChange(change) {
|
|
|
169
169
|
}
|
|
170
170
|
return annotated;
|
|
171
171
|
}
|
|
172
|
-
// Message-level changes: /
|
|
172
|
+
// Message-level changes: /protocols/websocket/channels/{index}/messages/{msgIndex}
|
|
173
173
|
if (next === "messages") {
|
|
174
|
-
const messageIndex = segments[
|
|
174
|
+
const messageIndex = segments[5];
|
|
175
175
|
if (typeof messageIndex === "undefined") {
|
|
176
176
|
return annotated;
|
|
177
177
|
}
|
|
@@ -187,16 +187,16 @@ function annotateWebSocketChange(change) {
|
|
|
187
187
|
return annotated;
|
|
188
188
|
}
|
|
189
189
|
function annotateGraphQLChange(change) {
|
|
190
|
-
if (!change.path.startsWith("/
|
|
190
|
+
if (!change.path.startsWith("/protocols/graphql/")) {
|
|
191
191
|
return change;
|
|
192
192
|
}
|
|
193
193
|
const segments = change.path.split("/").filter(Boolean);
|
|
194
|
-
// Expected: ["
|
|
195
|
-
if (segments[0] !== "
|
|
194
|
+
// Expected: ["protocols","graphql", kind, index, ...]
|
|
195
|
+
if (segments[0] !== "protocols" || segments[1] !== "graphql") {
|
|
196
196
|
return change;
|
|
197
197
|
}
|
|
198
|
-
const opKind = segments[
|
|
199
|
-
const index = segments[
|
|
198
|
+
const opKind = segments[2];
|
|
199
|
+
const index = segments[3];
|
|
200
200
|
if (!opKind || typeof index === "undefined") {
|
|
201
201
|
return change;
|
|
202
202
|
}
|
package/dist/normalizer/index.js
CHANGED
|
@@ -16,10 +16,10 @@ function normalizeValue(value) {
|
|
|
16
16
|
return value;
|
|
17
17
|
}
|
|
18
18
|
function normalizeRestRoutes(doc) {
|
|
19
|
-
if (!doc || !doc.
|
|
19
|
+
if (!doc || !doc.protocols) {
|
|
20
20
|
return doc;
|
|
21
21
|
}
|
|
22
|
-
const protocols = doc.
|
|
22
|
+
const protocols = doc.protocols;
|
|
23
23
|
const rest = protocols.rest;
|
|
24
24
|
if (!rest || !Array.isArray(rest.routes)) {
|
|
25
25
|
return doc;
|
|
@@ -34,10 +34,10 @@ function normalizeRestRoutes(doc) {
|
|
|
34
34
|
return doc;
|
|
35
35
|
}
|
|
36
36
|
function normalizeWebSocket(doc) {
|
|
37
|
-
if (!doc || !doc.
|
|
37
|
+
if (!doc || !doc.protocols) {
|
|
38
38
|
return doc;
|
|
39
39
|
}
|
|
40
|
-
const protocols = doc.
|
|
40
|
+
const protocols = doc.protocols;
|
|
41
41
|
const websocket = protocols.websocket;
|
|
42
42
|
if (!websocket || !Array.isArray(websocket.channels)) {
|
|
43
43
|
return doc;
|
|
@@ -65,10 +65,10 @@ function normalizeWebSocket(doc) {
|
|
|
65
65
|
return doc;
|
|
66
66
|
}
|
|
67
67
|
function normalizeGraphqlOperations(doc) {
|
|
68
|
-
if (!doc || !doc.
|
|
68
|
+
if (!doc || !doc.protocols) {
|
|
69
69
|
return doc;
|
|
70
70
|
}
|
|
71
|
-
const protocols = doc.
|
|
71
|
+
const protocols = doc.protocols;
|
|
72
72
|
const graphql = protocols.graphql;
|
|
73
73
|
if (!graphql) {
|
|
74
74
|
return doc;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -85,21 +85,12 @@ export interface UniSpecServiceProtocols {
|
|
|
85
85
|
graphql?: UniSpecGraphQLProtocol;
|
|
86
86
|
websocket?: UniSpecWebSocketProtocol;
|
|
87
87
|
}
|
|
88
|
-
export interface
|
|
88
|
+
export interface UniSpecDocument {
|
|
89
89
|
name: string;
|
|
90
|
-
title?: string;
|
|
91
90
|
description?: string;
|
|
92
91
|
version?: string;
|
|
93
|
-
owner?: string;
|
|
94
|
-
tags?: string[];
|
|
95
|
-
links?: Record<string, string>;
|
|
96
92
|
protocols?: UniSpecServiceProtocols;
|
|
97
93
|
schemas?: UniSpecSchemas;
|
|
98
|
-
environments?: UniSpecEnvironment[];
|
|
99
|
-
}
|
|
100
|
-
export interface UniSpecDocument {
|
|
101
|
-
unispecVersion: string;
|
|
102
|
-
service: UniSpecService;
|
|
103
94
|
extensions?: Record<string, unknown>;
|
|
104
95
|
}
|
|
105
96
|
export interface ValidationError {
|
package/dist/validator/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Ajv2020 from "ajv/dist/2020.js";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { createRequire } from "module";
|
|
4
5
|
import { unispec as unispecSchema, manifest as unispecManifest } from "@unispechq/unispec-schema";
|
|
5
6
|
const ajv = new Ajv2020({
|
|
6
7
|
allErrors: true,
|
|
@@ -8,16 +9,53 @@ const ajv = new Ajv2020({
|
|
|
8
9
|
});
|
|
9
10
|
// Register minimal URI format to satisfy UniSpec schemas (service.environments[*].baseUrl)
|
|
10
11
|
ajv.addFormat("uri", true);
|
|
12
|
+
function findPackageRoot(startFilePath) {
|
|
13
|
+
let dir = path.dirname(startFilePath);
|
|
14
|
+
for (let i = 0; i < 50; i++) {
|
|
15
|
+
const pkgJsonPath = path.join(dir, "package.json");
|
|
16
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
const parent = path.dirname(dir);
|
|
20
|
+
if (parent === dir) {
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`Cannot locate package.json for resolved path: ${startFilePath}`);
|
|
26
|
+
}
|
|
27
|
+
function getUniSpecSchemaDir() {
|
|
28
|
+
const localRequire = typeof require !== "undefined" ? require : createRequire(path.join(process.cwd(), "package.json"));
|
|
29
|
+
try {
|
|
30
|
+
const pkgJsonPath = localRequire.resolve("@unispechq/unispec-schema/package.json");
|
|
31
|
+
return path.join(path.dirname(pkgJsonPath), "schema");
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
const entryPath = localRequire.resolve("@unispechq/unispec-schema");
|
|
35
|
+
const pkgRoot = findPackageRoot(entryPath);
|
|
36
|
+
return path.join(pkgRoot, "schema");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
11
39
|
// Register all UniSpec subschemas so that Ajv can resolve internal $ref links
|
|
12
40
|
try {
|
|
13
|
-
const schemaDir =
|
|
41
|
+
const schemaDir = getUniSpecSchemaDir();
|
|
14
42
|
const types = unispecManifest?.types ?? {};
|
|
15
43
|
const typeSchemaPaths = Object.values(types).map((rel) => String(rel));
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
for (const relPath of typeSchemaPaths) {
|
|
45
|
+
try {
|
|
46
|
+
const filePath = path.join(schemaDir, relPath);
|
|
47
|
+
if (!fs.existsSync(filePath)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const schema = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
51
|
+
const normalizedRelPath = String(relPath).replace(/^\.\//, "");
|
|
52
|
+
const fallbackId = `https://unispec.dev/schema/${normalizedRelPath}`;
|
|
53
|
+
ajv.addSchema(schema, fallbackId);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Ignore individual schema registration failures to keep the validator usable.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
21
59
|
}
|
|
22
60
|
catch {
|
|
23
61
|
// If subschemas cannot be loaded for some reason, validation will still work for
|
|
@@ -38,7 +76,18 @@ function mapAjvErrors(errors) {
|
|
|
38
76
|
* Validate a UniSpec document against the UniSpec JSON Schema.
|
|
39
77
|
*/
|
|
40
78
|
export async function validateUniSpec(doc, _options = {}) {
|
|
41
|
-
const
|
|
79
|
+
const docForValidation = {
|
|
80
|
+
unispecVersion: "0.0.0",
|
|
81
|
+
service: {
|
|
82
|
+
name: doc.name,
|
|
83
|
+
description: doc.description,
|
|
84
|
+
version: doc.version,
|
|
85
|
+
protocols: doc.protocols,
|
|
86
|
+
schemas: doc.schemas,
|
|
87
|
+
},
|
|
88
|
+
extensions: doc.extensions,
|
|
89
|
+
};
|
|
90
|
+
const valid = validateFn(docForValidation);
|
|
42
91
|
if (valid) {
|
|
43
92
|
return {
|
|
44
93
|
valid: true,
|
|
@@ -55,7 +104,7 @@ export async function validateUniSpec(doc, _options = {}) {
|
|
|
55
104
|
*/
|
|
56
105
|
export async function validateUniSpecTests(doc, _options = {}) {
|
|
57
106
|
if (!validateTestsFn) {
|
|
58
|
-
const schemaDir =
|
|
107
|
+
const schemaDir = getUniSpecSchemaDir();
|
|
59
108
|
const testsSchemaPath = path.join(schemaDir, "unispec-tests.schema.json");
|
|
60
109
|
const testsSchema = JSON.parse(fs.readFileSync(testsSchemaPath, "utf8"));
|
|
61
110
|
validateTestsFn = ajv.compile(testsSchema);
|
package/package.json
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unispechq/unispec-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Central UniSpec Core Engine providing parsing, validation, normalization, diffing, and conversion of UniSpec specs.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/unispec/unispec-core.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/unispec/unispec-core/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/unispec/unispec-core#readme",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"unispec",
|
|
16
|
+
"specification",
|
|
17
|
+
"schema",
|
|
18
|
+
"validator",
|
|
19
|
+
"normalizer",
|
|
20
|
+
"diff",
|
|
21
|
+
"converter"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
6
26
|
"type": "module",
|
|
7
27
|
"main": "dist/index.cjs",
|
|
8
28
|
"module": "dist/index.js",
|
|
@@ -28,7 +48,7 @@
|
|
|
28
48
|
"release:major": "node scripts/release.js major"
|
|
29
49
|
},
|
|
30
50
|
"dependencies": {
|
|
31
|
-
"@unispechq/unispec-schema": "^0.3.
|
|
51
|
+
"@unispechq/unispec-schema": "^0.3.3",
|
|
32
52
|
"ajv": "^8.12.0"
|
|
33
53
|
},
|
|
34
54
|
"devDependencies": {
|