@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.
@@ -221,12 +221,25 @@ function annotateGraphQLChange(change) {
221
221
  return annotated;
222
222
  }
223
223
  /**
224
- * Compute a structural diff between two UniSpec documents.
224
+ * Compare two UniSpec documents and return detected changes.
225
225
  *
226
- * Current behavior:
227
- * - Tracks added, removed, and changed fields and array items.
228
- * - Uses JSON Pointer-like paths rooted at "" (e.g., "/info/title").
229
- * - Marks all changes with severity "unknown" for now.
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);
@@ -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
- * Currently supports:
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, _options = {}) {
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
- // For now we assume JSON; YAML support will be added later.
19
- return JSON.parse(trimmed);
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
- * - Preserves values as-is.
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
+ }