@unispechq/unispec-core 0.2.9 → 0.2.11
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/diff/index.js +18 -5
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/loader/index.js +61 -6
- package/dist/cjs/normalizer/index.js +16 -2
- package/dist/cjs/schemas/index.js +131 -0
- package/dist/cjs/validator/generated-schemas.js +819 -0
- package/dist/cjs/validator/index.js +5 -82
- package/dist/diff/index.d.ts +18 -5
- package/dist/diff/index.js +18 -5
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/loader/index.d.ts +3 -4
- package/dist/loader/index.js +28 -6
- package/dist/normalizer/index.d.ts +16 -2
- package/dist/normalizer/index.js +16 -2
- package/dist/schemas/index.d.ts +4 -0
- package/dist/schemas/index.js +126 -0
- package/dist/validator/generated-schemas.d.ts +832 -0
- package/dist/validator/generated-schemas.js +816 -0
- package/dist/validator/index.js +5 -49
- package/package.json +8 -3
package/dist/cjs/diff/index.js
CHANGED
|
@@ -221,12 +221,25 @@ function annotateGraphQLChange(change) {
|
|
|
221
221
|
return annotated;
|
|
222
222
|
}
|
|
223
223
|
/**
|
|
224
|
-
*
|
|
224
|
+
* Compare two UniSpec documents and return detected changes.
|
|
225
225
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* -
|
|
229
|
-
* -
|
|
226
|
+
* This function performs a deep comparison between two UniSpec documents
|
|
227
|
+
* and categorizes changes by severity and protocol. Useful for:
|
|
228
|
+
* - API change detection in CI/CD pipelines
|
|
229
|
+
* - Breaking change analysis
|
|
230
|
+
* - Version compatibility checks
|
|
231
|
+
* - Audit trails for API evolution
|
|
232
|
+
*
|
|
233
|
+
* Features:
|
|
234
|
+
* - Detects added/removed fields at any depth
|
|
235
|
+
* - Special handling for named collections (routes, operations, channels)
|
|
236
|
+
* - Severity classification: "breaking" | "non-breaking" | "unknown"
|
|
237
|
+
* - Protocol categorization: "rest" | "graphql" | "websocket"
|
|
238
|
+
* - Detailed change descriptions with JSON paths
|
|
239
|
+
*
|
|
240
|
+
* @param oldDoc - The previous version of the UniSpec document
|
|
241
|
+
* @param newDoc - The current version of the UniSpec document
|
|
242
|
+
* @returns Object containing all detected changes
|
|
230
243
|
*/
|
|
231
244
|
function diffUniSpec(oldDoc, newDoc) {
|
|
232
245
|
const changes = [];
|
package/dist/cjs/index.js
CHANGED
|
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./types/index.js"), exports);
|
|
18
18
|
__exportStar(require("./loader/index.js"), exports);
|
|
19
19
|
__exportStar(require("./validator/index.js"), exports);
|
|
20
|
+
__exportStar(require("./schemas/index.js"), exports);
|
|
20
21
|
__exportStar(require("./normalizer/index.js"), exports);
|
|
21
22
|
__exportStar(require("./diff/index.js"), exports);
|
|
22
23
|
__exportStar(require("./converters/index.js"), exports);
|
package/dist/cjs/loader/index.js
CHANGED
|
@@ -1,22 +1,77 @@
|
|
|
1
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.loadUniSpec = loadUniSpec;
|
|
4
37
|
/**
|
|
5
38
|
* Load a UniSpec document from a raw input value.
|
|
6
|
-
*
|
|
39
|
+
* Supports:
|
|
7
40
|
* - JavaScript objects (treated as already parsed UniSpec)
|
|
8
41
|
* - JSON strings
|
|
9
|
-
*
|
|
10
|
-
* YAML and filesystem helpers will be added later, keeping this API stable.
|
|
42
|
+
* - YAML strings
|
|
11
43
|
*/
|
|
12
|
-
async function loadUniSpec(input,
|
|
44
|
+
async function loadUniSpec(input, options = {}) {
|
|
13
45
|
if (typeof input === "string") {
|
|
14
46
|
const trimmed = input.trim();
|
|
15
47
|
if (!trimmed) {
|
|
16
48
|
throw new Error("Cannot load UniSpec: input string is empty");
|
|
17
49
|
}
|
|
18
|
-
//
|
|
19
|
-
|
|
50
|
+
// Try JSON first (faster and more common)
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(trimmed);
|
|
53
|
+
}
|
|
54
|
+
catch (jsonError) {
|
|
55
|
+
// If JSON fails, try YAML
|
|
56
|
+
try {
|
|
57
|
+
// Dynamic import to avoid bundling yaml parser if not needed
|
|
58
|
+
const yamlModule = await Promise.resolve().then(() => __importStar(require("js-yaml")));
|
|
59
|
+
if (!yamlModule.load) {
|
|
60
|
+
throw new Error("js-yaml module not available");
|
|
61
|
+
}
|
|
62
|
+
const doc = yamlModule.load(trimmed, {
|
|
63
|
+
filename: options.filename,
|
|
64
|
+
// Basic security options
|
|
65
|
+
schema: yamlModule.FAILSAFE_SCHEMA
|
|
66
|
+
});
|
|
67
|
+
return doc;
|
|
68
|
+
}
|
|
69
|
+
catch (yamlError) {
|
|
70
|
+
const jsonMsg = jsonError instanceof Error ? jsonError.message : String(jsonError);
|
|
71
|
+
const yamlMsg = yamlError instanceof Error ? yamlError.message : String(yamlError);
|
|
72
|
+
throw new Error(`Failed to parse input as JSON or YAML. JSON error: ${jsonMsg}. YAML error: ${yamlMsg}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
20
75
|
}
|
|
21
76
|
return input;
|
|
22
77
|
}
|
|
@@ -97,9 +97,23 @@ function normalizeGraphqlOperations(doc) {
|
|
|
97
97
|
/**
|
|
98
98
|
* Normalize a UniSpec document into a canonical, deterministic form.
|
|
99
99
|
*
|
|
100
|
+
* This function ensures consistent representation of UniSpec documents
|
|
101
|
+
* by sorting object keys and protocol-specific structures in a predictable order.
|
|
102
|
+
* Useful for:
|
|
103
|
+
* - Generating stable diffs between documents
|
|
104
|
+
* - Creating consistent output for caching
|
|
105
|
+
* - Ensuring reproducible builds
|
|
106
|
+
*
|
|
100
107
|
* Current behavior:
|
|
101
|
-
* - Recursively sorts object keys lexicographically
|
|
102
|
-
* -
|
|
108
|
+
* - Recursively sorts object keys lexicographically
|
|
109
|
+
* - Sorts REST routes by name or path+method
|
|
110
|
+
* - Sorts GraphQL operations by name within each operation type
|
|
111
|
+
* - Sorts WebSocket channels and messages by name
|
|
112
|
+
* - Preserves all values as-is
|
|
113
|
+
*
|
|
114
|
+
* @param doc - The UniSpec document to normalize
|
|
115
|
+
* @param options - Normalization options (currently unused, reserved for future)
|
|
116
|
+
* @returns The normalized UniSpec document
|
|
103
117
|
*/
|
|
104
118
|
function normalizeUniSpec(doc, _options = {}) {
|
|
105
119
|
const normalized = normalizeValue(doc);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveSchemaRef = resolveSchemaRef;
|
|
4
|
+
exports.registerSchema = registerSchema;
|
|
5
|
+
exports.dedupeSchemas = dedupeSchemas;
|
|
6
|
+
function normalizeSchemaRef(ref) {
|
|
7
|
+
const trimmed = ref.trim();
|
|
8
|
+
if (trimmed.length === 0)
|
|
9
|
+
return "";
|
|
10
|
+
const withoutHash = trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
|
|
11
|
+
const parts = withoutHash.split("/").filter(Boolean);
|
|
12
|
+
return parts.length > 0 ? parts[parts.length - 1] : withoutHash;
|
|
13
|
+
}
|
|
14
|
+
function stableStringify(value) {
|
|
15
|
+
if (value === null)
|
|
16
|
+
return "null";
|
|
17
|
+
const t = typeof value;
|
|
18
|
+
if (t === "number" || t === "boolean")
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
if (t === "string")
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
if (Array.isArray(value))
|
|
23
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
24
|
+
if (t !== "object")
|
|
25
|
+
return JSON.stringify(value);
|
|
26
|
+
const obj = value;
|
|
27
|
+
const keys = Object.keys(obj).sort();
|
|
28
|
+
const entries = keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`);
|
|
29
|
+
return `{${entries.join(",")}}`;
|
|
30
|
+
}
|
|
31
|
+
function resolveSchemaRef(doc, ref) {
|
|
32
|
+
const name = normalizeSchemaRef(ref);
|
|
33
|
+
if (!name)
|
|
34
|
+
return undefined;
|
|
35
|
+
return doc.schemas?.[name];
|
|
36
|
+
}
|
|
37
|
+
function registerSchema(doc, name, jsonSchema) {
|
|
38
|
+
if (!doc.schemas) {
|
|
39
|
+
doc.schemas = {};
|
|
40
|
+
}
|
|
41
|
+
const definition = {
|
|
42
|
+
jsonSchema,
|
|
43
|
+
};
|
|
44
|
+
doc.schemas[name] = definition;
|
|
45
|
+
return definition;
|
|
46
|
+
}
|
|
47
|
+
function updateSchemaRefs(doc, mapping) {
|
|
48
|
+
const rest = doc.protocols?.rest;
|
|
49
|
+
const websocket = doc.protocols?.websocket;
|
|
50
|
+
if (rest?.routes) {
|
|
51
|
+
for (const route of rest.routes) {
|
|
52
|
+
for (const params of [route.pathParams, route.queryParams, route.headers]) {
|
|
53
|
+
if (!params)
|
|
54
|
+
continue;
|
|
55
|
+
for (const p of params) {
|
|
56
|
+
if (!p.schemaRef)
|
|
57
|
+
continue;
|
|
58
|
+
const key = normalizeSchemaRef(p.schemaRef);
|
|
59
|
+
const next = mapping[key];
|
|
60
|
+
if (next && next !== key)
|
|
61
|
+
p.schemaRef = next;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const requestContent = route.requestBody?.content;
|
|
65
|
+
if (requestContent) {
|
|
66
|
+
for (const media of Object.values(requestContent)) {
|
|
67
|
+
if (!media.schemaRef)
|
|
68
|
+
continue;
|
|
69
|
+
const key = normalizeSchemaRef(media.schemaRef);
|
|
70
|
+
const next = mapping[key];
|
|
71
|
+
if (next && next !== key)
|
|
72
|
+
media.schemaRef = next;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const responses = route.responses;
|
|
76
|
+
if (responses) {
|
|
77
|
+
for (const resp of Object.values(responses)) {
|
|
78
|
+
const respContent = resp.content;
|
|
79
|
+
if (!respContent)
|
|
80
|
+
continue;
|
|
81
|
+
for (const media of Object.values(respContent)) {
|
|
82
|
+
if (!media.schemaRef)
|
|
83
|
+
continue;
|
|
84
|
+
const key = normalizeSchemaRef(media.schemaRef);
|
|
85
|
+
const next = mapping[key];
|
|
86
|
+
if (next && next !== key)
|
|
87
|
+
media.schemaRef = next;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (websocket?.channels) {
|
|
94
|
+
for (const channel of websocket.channels) {
|
|
95
|
+
if (!channel.messages)
|
|
96
|
+
continue;
|
|
97
|
+
for (const message of channel.messages) {
|
|
98
|
+
if (!message.schemaRef)
|
|
99
|
+
continue;
|
|
100
|
+
const key = normalizeSchemaRef(message.schemaRef);
|
|
101
|
+
const next = mapping[key];
|
|
102
|
+
if (next && next !== key)
|
|
103
|
+
message.schemaRef = next;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function dedupeSchemas(doc) {
|
|
109
|
+
if (!doc.schemas)
|
|
110
|
+
return;
|
|
111
|
+
const names = Object.keys(doc.schemas);
|
|
112
|
+
const hashToName = new Map();
|
|
113
|
+
const renameMap = {};
|
|
114
|
+
for (const name of names) {
|
|
115
|
+
const schema = doc.schemas[name];
|
|
116
|
+
const hash = stableStringify(schema?.jsonSchema ?? null);
|
|
117
|
+
const existing = hashToName.get(hash);
|
|
118
|
+
if (!existing) {
|
|
119
|
+
hashToName.set(hash, name);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
renameMap[name] = existing;
|
|
123
|
+
}
|
|
124
|
+
const duplicates = Object.keys(renameMap);
|
|
125
|
+
if (duplicates.length === 0)
|
|
126
|
+
return;
|
|
127
|
+
updateSchemaRefs(doc, renameMap);
|
|
128
|
+
for (const dup of duplicates) {
|
|
129
|
+
delete doc.schemas[dup];
|
|
130
|
+
}
|
|
131
|
+
}
|