@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.
@@ -4,23 +4,16 @@ exports.toOpenAPI = toOpenAPI;
4
4
  exports.toGraphQLSDL = toGraphQLSDL;
5
5
  exports.toWebSocketModel = toWebSocketModel;
6
6
  function toOpenAPI(doc) {
7
- const service = doc.service;
8
- const rest = (service.protocols?.rest ?? {});
7
+ const rest = (doc.protocols?.rest ?? {});
9
8
  const info = {
10
- title: service.title ?? service.name,
11
- description: service.description,
9
+ title: doc.name,
10
+ description: doc.description,
12
11
  };
13
- // Derive OpenAPI servers from UniSpec environments when available.
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 service.schemas into OpenAPI components.schemas
23
- const schemas = (service.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.service.protocols?.graphql;
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 service = doc.service;
154
- const title = service.title ?? service.name;
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 service = doc.service;
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: service.name,
199
- title: service.title,
200
- description: service.description,
189
+ name: doc.name,
190
+ title: undefined,
191
+ description: doc.description,
201
192
  },
202
193
  channels,
203
194
  rawProtocol: websocket,
@@ -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 === "/service/protocols/rest/routes" ||
44
- basePath === "/service/protocols/websocket/channels" ||
45
- basePath === "/service/protocols/graphql/queries" ||
46
- basePath === "/service/protocols/graphql/mutations" ||
47
- basePath === "/service/protocols/graphql/subscriptions";
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("/service/protocols/rest/routes/")) {
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: ["service", "protocols", "rest", "routes", index]
124
- if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "rest" || segments[3] !== "routes") {
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[4];
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("/service/protocols/websocket/channels/")) {
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: ["service","protocols","websocket","channels", channelIndex, ...]
151
- if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "websocket" || segments[3] !== "channels") {
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[4];
155
- const next = segments[5];
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: /service/protocols/websocket/channels/{index}
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: /service/protocols/websocket/channels/{index}/messages/{msgIndex}
175
+ // Message-level changes: /protocols/websocket/channels/{index}/messages/{msgIndex}
176
176
  if (next === "messages") {
177
- const messageIndex = segments[6];
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("/service/protocols/graphql/")) {
193
+ if (!change.path.startsWith("/protocols/graphql/")) {
194
194
  return change;
195
195
  }
196
196
  const segments = change.path.split("/").filter(Boolean);
197
- // Expected: ["service","protocols","graphql", kind, index, ...]
198
- if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "graphql") {
197
+ // Expected: ["protocols","graphql", kind, index, ...]
198
+ if (segments[0] !== "protocols" || segments[1] !== "graphql") {
199
199
  return change;
200
200
  }
201
- const opKind = segments[3];
202
- const index = segments[4];
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.service || !doc.service.protocols) {
22
+ if (!doc || !doc.protocols) {
23
23
  return doc;
24
24
  }
25
- const protocols = doc.service.protocols;
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.service || !doc.service.protocols) {
40
+ if (!doc || !doc.protocols) {
41
41
  return doc;
42
42
  }
43
- const protocols = doc.service.protocols;
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.service || !doc.service.protocols) {
71
+ if (!doc || !doc.protocols) {
72
72
  return doc;
73
73
  }
74
- const protocols = doc.service.protocols;
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 = path_1.default.join(process.cwd(), "node_modules", "@unispechq", "unispec-schema", "schema");
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 loadedTypeSchemas = typeSchemaPaths
24
- .map((relPath) => path_1.default.join(schemaDir, relPath))
25
- .filter((filePath) => fs_1.default.existsSync(filePath))
26
- .map((filePath) => JSON.parse(fs_1.default.readFileSync(filePath, "utf8")));
27
- ajv.addSchema(loadedTypeSchemas);
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 valid = validateFn(doc);
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 = path_1.default.join(process.cwd(), "node_modules", "@unispechq", "unispec-schema", "schema");
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);
@@ -1,21 +1,14 @@
1
1
  export function toOpenAPI(doc) {
2
- const service = doc.service;
3
- const rest = (service.protocols?.rest ?? {});
2
+ const rest = (doc.protocols?.rest ?? {});
4
3
  const info = {
5
- title: service.title ?? service.name,
6
- description: service.description,
4
+ title: doc.name,
5
+ description: doc.description,
7
6
  };
8
- // Derive OpenAPI servers from UniSpec environments when available.
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 service.schemas into OpenAPI components.schemas
18
- const schemas = (service.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.service.protocols?.graphql;
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 service = doc.service;
149
- const title = service.title ?? service.name;
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 service = doc.service;
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: service.name,
194
- title: service.title,
195
- description: service.description,
184
+ name: doc.name,
185
+ title: undefined,
186
+ description: doc.description,
196
187
  },
197
188
  channels,
198
189
  rawProtocol: websocket,
@@ -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 === "/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";
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("/service/protocols/rest/routes/")) {
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: ["service", "protocols", "rest", "routes", index]
121
- if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "rest" || segments[3] !== "routes") {
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[4];
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("/service/protocols/websocket/channels/")) {
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: ["service","protocols","websocket","channels", channelIndex, ...]
148
- if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "websocket" || segments[3] !== "channels") {
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[4];
152
- const next = segments[5];
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: /service/protocols/websocket/channels/{index}
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: /service/protocols/websocket/channels/{index}/messages/{msgIndex}
172
+ // Message-level changes: /protocols/websocket/channels/{index}/messages/{msgIndex}
173
173
  if (next === "messages") {
174
- const messageIndex = segments[6];
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("/service/protocols/graphql/")) {
190
+ if (!change.path.startsWith("/protocols/graphql/")) {
191
191
  return change;
192
192
  }
193
193
  const segments = change.path.split("/").filter(Boolean);
194
- // Expected: ["service","protocols","graphql", kind, index, ...]
195
- if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "graphql") {
194
+ // Expected: ["protocols","graphql", kind, index, ...]
195
+ if (segments[0] !== "protocols" || segments[1] !== "graphql") {
196
196
  return change;
197
197
  }
198
- const opKind = segments[3];
199
- const index = segments[4];
198
+ const opKind = segments[2];
199
+ const index = segments[3];
200
200
  if (!opKind || typeof index === "undefined") {
201
201
  return change;
202
202
  }
@@ -16,10 +16,10 @@ function normalizeValue(value) {
16
16
  return value;
17
17
  }
18
18
  function normalizeRestRoutes(doc) {
19
- if (!doc || !doc.service || !doc.service.protocols) {
19
+ if (!doc || !doc.protocols) {
20
20
  return doc;
21
21
  }
22
- const protocols = doc.service.protocols;
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.service || !doc.service.protocols) {
37
+ if (!doc || !doc.protocols) {
38
38
  return doc;
39
39
  }
40
- const protocols = doc.service.protocols;
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.service || !doc.service.protocols) {
68
+ if (!doc || !doc.protocols) {
69
69
  return doc;
70
70
  }
71
- const protocols = doc.service.protocols;
71
+ const protocols = doc.protocols;
72
72
  const graphql = protocols.graphql;
73
73
  if (!graphql) {
74
74
  return doc;
@@ -85,21 +85,12 @@ export interface UniSpecServiceProtocols {
85
85
  graphql?: UniSpecGraphQLProtocol;
86
86
  websocket?: UniSpecWebSocketProtocol;
87
87
  }
88
- export interface UniSpecService {
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 {
@@ -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 = path.join(process.cwd(), "node_modules", "@unispechq", "unispec-schema", "schema");
41
+ const schemaDir = getUniSpecSchemaDir();
14
42
  const types = unispecManifest?.types ?? {};
15
43
  const typeSchemaPaths = Object.values(types).map((rel) => String(rel));
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")));
20
- ajv.addSchema(loadedTypeSchemas);
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 valid = validateFn(doc);
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 = path.join(process.cwd(), "node_modules", "@unispechq", "unispec-schema", "schema");
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.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.1",
51
+ "@unispechq/unispec-schema": "^0.3.3",
32
52
  "ajv": "^8.12.0"
33
53
  },
34
54
  "devDependencies": {