@unispechq/unispec-core 0.2.10 → 0.2.12

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.
@@ -4,16 +4,16 @@ exports.toOpenAPI = toOpenAPI;
4
4
  exports.toGraphQLSDL = toGraphQLSDL;
5
5
  exports.toWebSocketModel = toWebSocketModel;
6
6
  function toOpenAPI(doc) {
7
- const rest = (doc.protocols?.rest ?? {});
7
+ const rest = (doc.service?.protocols?.rest ?? {});
8
8
  const info = {
9
- title: doc.name,
10
- description: doc.description,
9
+ title: doc.service.name,
10
+ description: doc.service.description,
11
11
  };
12
12
  const servers = [];
13
13
  const paths = {};
14
14
  const components = {};
15
- // Map doc.schemas into OpenAPI components.schemas
16
- const schemas = (doc.schemas ?? {});
15
+ // Map doc.service.schemas into OpenAPI components.schemas
16
+ const schemas = (doc.service?.schemas ?? {});
17
17
  const componentsSchemas = {};
18
18
  for (const [name, def] of Object.entries(schemas)) {
19
19
  componentsSchemas[name] = def.jsonSchema;
@@ -138,13 +138,16 @@ function toGraphQLSDL(doc) {
138
138
  // via a Query field. This does not attempt to interpret the full GraphQL
139
139
  // protocol structure yet, but provides a stable, deterministic SDL shape
140
140
  // based on top-level UniSpec document fields.
141
- const graphql = doc.protocols?.graphql;
141
+ const graphql = doc.service?.protocols?.graphql;
142
142
  const customSDL = graphql?.schema;
143
143
  if (typeof customSDL === "string" && customSDL.trim()) {
144
- return { sdl: customSDL };
144
+ return {
145
+ sdl: customSDL,
146
+ url: graphql?.url
147
+ };
145
148
  }
146
- const title = doc.name;
147
- const description = doc.description ?? "";
149
+ const title = doc.service.name;
150
+ const description = doc.service.description ?? "";
148
151
  const lines = [];
149
152
  if (title || description) {
150
153
  lines.push("\"\"");
@@ -165,14 +168,17 @@ function toGraphQLSDL(doc) {
165
168
  lines.push(" _serviceInfo: String!\n");
166
169
  lines.push("}");
167
170
  const sdl = lines.join("\n");
168
- return { sdl };
171
+ return {
172
+ sdl,
173
+ url: graphql?.url
174
+ };
169
175
  }
170
176
  function toWebSocketModel(doc) {
171
177
  // Base WebSocket model intended for a modern, dashboard-oriented UI.
172
178
  // It exposes service metadata, a normalized list of channels and the raw
173
179
  // websocket protocol object, while also embedding the original UniSpec
174
180
  // document under a technical key for debugging and introspection.
175
- const websocket = (doc.protocols?.websocket ?? {});
181
+ const websocket = (doc.service?.protocols?.websocket ?? {});
176
182
  const channelsArray = Array.isArray(websocket.channels) ? websocket.channels : [];
177
183
  const channels = [...channelsArray]
178
184
  .sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""))
@@ -186,10 +192,11 @@ function toWebSocketModel(doc) {
186
192
  }));
187
193
  return {
188
194
  service: {
189
- name: doc.name,
195
+ name: doc.service.name,
190
196
  title: undefined,
191
- description: doc.description,
197
+ description: doc.service.description,
192
198
  },
199
+ url: websocket.url,
193
200
  channels,
194
201
  rawProtocol: websocket,
195
202
  "x-unispec-ws": doc,
@@ -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 = [];
@@ -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
  }
@@ -19,10 +19,10 @@ function normalizeValue(value) {
19
19
  return value;
20
20
  }
21
21
  function normalizeRestRoutes(doc) {
22
- if (!doc || !doc.protocols) {
22
+ if (!doc || !doc.service?.protocols) {
23
23
  return doc;
24
24
  }
25
- const protocols = doc.protocols;
25
+ const protocols = doc.service.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.protocols) {
40
+ if (!doc || !doc.service?.protocols) {
41
41
  return doc;
42
42
  }
43
- const protocols = doc.protocols;
43
+ const protocols = doc.service.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.protocols) {
71
+ if (!doc || !doc.service?.protocols) {
72
72
  return doc;
73
73
  }
74
- const protocols = doc.protocols;
74
+ const protocols = doc.service.protocols;
75
75
  const graphql = protocols.graphql;
76
76
  if (!graphql) {
77
77
  return doc;
@@ -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);
@@ -32,21 +32,24 @@ function resolveSchemaRef(doc, ref) {
32
32
  const name = normalizeSchemaRef(ref);
33
33
  if (!name)
34
34
  return undefined;
35
- return doc.schemas?.[name];
35
+ return doc.service?.schemas?.[name];
36
36
  }
37
37
  function registerSchema(doc, name, jsonSchema) {
38
- if (!doc.schemas) {
39
- doc.schemas = {};
38
+ if (!doc.service) {
39
+ doc.service = { name: "" };
40
+ }
41
+ if (!doc.service.schemas) {
42
+ doc.service.schemas = {};
40
43
  }
41
44
  const definition = {
42
45
  jsonSchema,
43
46
  };
44
- doc.schemas[name] = definition;
47
+ doc.service.schemas[name] = definition;
45
48
  return definition;
46
49
  }
47
50
  function updateSchemaRefs(doc, mapping) {
48
- const rest = doc.protocols?.rest;
49
- const websocket = doc.protocols?.websocket;
51
+ const rest = doc.service?.protocols?.rest;
52
+ const websocket = doc.service?.protocols?.websocket;
50
53
  if (rest?.routes) {
51
54
  for (const route of rest.routes) {
52
55
  for (const params of [route.pathParams, route.queryParams, route.headers]) {
@@ -106,13 +109,13 @@ function updateSchemaRefs(doc, mapping) {
106
109
  }
107
110
  }
108
111
  function dedupeSchemas(doc) {
109
- if (!doc.schemas)
112
+ if (!doc.service?.schemas)
110
113
  return;
111
- const names = Object.keys(doc.schemas);
114
+ const names = Object.keys(doc.service.schemas);
112
115
  const hashToName = new Map();
113
116
  const renameMap = {};
114
117
  for (const name of names) {
115
- const schema = doc.schemas[name];
118
+ const schema = doc.service.schemas[name];
116
119
  const hash = stableStringify(schema?.jsonSchema ?? null);
117
120
  const existing = hashToName.get(hash);
118
121
  if (!existing) {
@@ -126,6 +129,6 @@ function dedupeSchemas(doc) {
126
129
  return;
127
130
  updateSchemaRefs(doc, renameMap);
128
131
  for (const dup of duplicates) {
129
- delete doc.schemas[dup];
132
+ delete doc.service.schemas[dup];
130
133
  }
131
134
  }