@unispechq/unispec-core 0.2.8 → 0.2.10

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/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);
@@ -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
+ }
@@ -1,4 +1,37 @@
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
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -6,32 +39,91 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.validateUniSpec = validateUniSpec;
7
40
  exports.validateUniSpecTests = validateUniSpecTests;
8
41
  const _2020_js_1 = __importDefault(require("ajv/dist/2020.js"));
9
- const fs_1 = __importDefault(require("fs"));
10
- const path_1 = __importDefault(require("path"));
11
- const unispec_schema_1 = require("@unispechq/unispec-schema");
12
- const ajv = new _2020_js_1.default({
13
- allErrors: true,
14
- strict: true,
15
- });
16
- // Register minimal URI format to satisfy UniSpec schemas (service.environments[*].baseUrl)
17
- ajv.addFormat("uri", true);
18
- // Register all UniSpec subschemas so that Ajv can resolve internal $ref links
19
- try {
20
- const schemaDir = path_1.default.join(process.cwd(), "node_modules", "@unispechq", "unispec-schema", "schema");
21
- const types = unispec_schema_1.manifest?.types ?? {};
42
+ let cached = null;
43
+ async function tryCreateNodeSchemaProvider() {
44
+ try {
45
+ const fs = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
46
+ const path = await Promise.resolve().then(() => __importStar(require("node:path")));
47
+ const module = await Promise.resolve().then(() => __importStar(require("node:module")));
48
+ const requireBasePath = typeof __filename === "string" && __filename.length > 0
49
+ ? __filename
50
+ : path.join(process.cwd(), "__unispec_core_require__.js");
51
+ const require = module.createRequire(requireBasePath);
52
+ const schemaIndexPath = require.resolve("@unispechq/unispec-schema/schema");
53
+ const schemaRoot = path.dirname(schemaIndexPath);
54
+ return {
55
+ getSchema: async (schemaPath) => {
56
+ const fullPath = path.join(schemaRoot, schemaPath);
57
+ const json = await fs.readFile(fullPath, "utf8");
58
+ return JSON.parse(json);
59
+ },
60
+ };
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ async function loadDefaultSchemas() {
67
+ const schemaProvider = await tryCreateNodeSchemaProvider();
68
+ if (!schemaProvider) {
69
+ throw new Error("UniSpec validator: default schema loading is not available in this environment. " +
70
+ "Provide ValidateOptions.schemas (recommended for browsers/edge runtimes) or ValidateOptions.schemaProvider.");
71
+ }
72
+ const { manifest, unispec } = (await Promise.resolve().then(() => __importStar(require("@unispechq/unispec-schema"))));
73
+ const types = manifest?.types ?? {};
22
74
  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);
75
+ const subschemas = await Promise.all(typeSchemaPaths.map(async (relPath) => {
76
+ try {
77
+ return await schemaProvider.getSchema(relPath);
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }));
83
+ let unispecTests;
84
+ try {
85
+ unispecTests = await schemaProvider.getSchema("unispec-tests.schema.json");
86
+ }
87
+ catch {
88
+ unispecTests = undefined;
89
+ }
90
+ return {
91
+ unispec: unispec,
92
+ unispecTests,
93
+ subschemas: subschemas.filter((s) => Boolean(s)),
94
+ };
95
+ }
96
+ function createAjv(options) {
97
+ const ajv = new _2020_js_1.default({
98
+ allErrors: true,
99
+ strict: true,
100
+ ...(options.ajvOptions ?? {}),
101
+ });
102
+ ajv.addFormat("uri", true);
103
+ return ajv;
28
104
  }
29
- catch {
30
- // If subschemas cannot be loaded for some reason, validation will still work for
31
- // parts of the schema that do not rely on those $ref references.
105
+ async function getValidator(options = {}) {
106
+ if (cached && !options.schemas && !options.schemaProvider && !options.ajvOptions)
107
+ return cached;
108
+ const schemas = options.schemas ?? (await loadDefaultSchemas());
109
+ const ajv = createAjv(options);
110
+ if (schemas.subschemas?.length) {
111
+ ajv.addSchema(schemas.subschemas);
112
+ }
113
+ const validateUniSpecFn = ajv.compile(schemas.unispec);
114
+ let validateUniSpecTestsFn;
115
+ if (schemas.unispecTests) {
116
+ validateUniSpecTestsFn = ajv.compile(schemas.unispecTests);
117
+ }
118
+ const result = {
119
+ validateUniSpecFn,
120
+ validateUniSpecTestsFn,
121
+ };
122
+ if (!options.schemas && !options.schemaProvider && !options.ajvOptions) {
123
+ cached = result;
124
+ }
125
+ return result;
32
126
  }
33
- const validateFn = ajv.compile(unispec_schema_1.unispec);
34
- let validateTestsFn;
35
127
  function mapAjvErrors(errors) {
36
128
  if (!errors)
37
129
  return [];
@@ -44,7 +136,8 @@ function mapAjvErrors(errors) {
44
136
  /**
45
137
  * Validate a UniSpec document against the UniSpec JSON Schema.
46
138
  */
47
- async function validateUniSpec(doc, _options = {}) {
139
+ async function validateUniSpec(doc, options = {}) {
140
+ const { validateUniSpecFn } = await getValidator(options);
48
141
  const docForValidation = {
49
142
  unispecVersion: "0.0.0",
50
143
  service: {
@@ -56,7 +149,7 @@ async function validateUniSpec(doc, _options = {}) {
56
149
  },
57
150
  extensions: doc.extensions,
58
151
  };
59
- const valid = validateFn(docForValidation);
152
+ const valid = validateUniSpecFn(docForValidation);
60
153
  if (valid) {
61
154
  return {
62
155
  valid: true,
@@ -65,20 +158,27 @@ async function validateUniSpec(doc, _options = {}) {
65
158
  }
66
159
  return {
67
160
  valid: false,
68
- errors: mapAjvErrors(validateFn.errors),
161
+ errors: mapAjvErrors(validateUniSpecFn.errors),
69
162
  };
70
163
  }
71
164
  /**
72
165
  * Validate a UniSpec Tests document against the UniSpec Tests JSON Schema.
73
166
  */
74
- async function validateUniSpecTests(doc, _options = {}) {
75
- if (!validateTestsFn) {
76
- const schemaDir = path_1.default.join(process.cwd(), "node_modules", "@unispechq", "unispec-schema", "schema");
77
- const testsSchemaPath = path_1.default.join(schemaDir, "unispec-tests.schema.json");
78
- const testsSchema = JSON.parse(fs_1.default.readFileSync(testsSchemaPath, "utf8"));
79
- validateTestsFn = ajv.compile(testsSchema);
167
+ async function validateUniSpecTests(doc, options = {}) {
168
+ const { validateUniSpecTestsFn } = await getValidator(options);
169
+ if (!validateUniSpecTestsFn) {
170
+ return {
171
+ valid: false,
172
+ errors: [
173
+ {
174
+ message: "UniSpec tests validation schema is not available. Provide ValidateOptions.schemas.unispecTests or a schemaProvider that can load it.",
175
+ path: "",
176
+ code: "schema_missing",
177
+ },
178
+ ],
179
+ };
80
180
  }
81
- const valid = validateTestsFn(doc);
181
+ const valid = validateUniSpecTestsFn(doc);
82
182
  if (valid) {
83
183
  return {
84
184
  valid: true,
@@ -87,6 +187,6 @@ async function validateUniSpecTests(doc, _options = {}) {
87
187
  }
88
188
  return {
89
189
  valid: false,
90
- errors: mapAjvErrors(validateTestsFn.errors),
190
+ errors: mapAjvErrors(validateUniSpecTestsFn.errors),
91
191
  };
92
192
  }
package/dist/index.cjs 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/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./types/index.js";
2
2
  export * from "./loader/index.js";
3
3
  export * from "./validator/index.js";
4
+ export * from "./schemas/index.js";
4
5
  export * from "./normalizer/index.js";
5
6
  export * from "./diff/index.js";
6
7
  export * from "./converters/index.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./types/index.js";
2
2
  export * from "./loader/index.js";
3
3
  export * from "./validator/index.js";
4
+ export * from "./schemas/index.js";
4
5
  export * from "./normalizer/index.js";
5
6
  export * from "./diff/index.js";
6
7
  export * from "./converters/index.js";
@@ -0,0 +1,4 @@
1
+ import { UniSpecDocument, UniSpecSchemaDefinition } from "../types/index.js";
2
+ export declare function resolveSchemaRef(doc: UniSpecDocument, ref: string): UniSpecSchemaDefinition | undefined;
3
+ export declare function registerSchema(doc: UniSpecDocument, name: string, jsonSchema: Record<string, unknown>): UniSpecSchemaDefinition;
4
+ export declare function dedupeSchemas(doc: UniSpecDocument): void;
@@ -0,0 +1,126 @@
1
+ function normalizeSchemaRef(ref) {
2
+ const trimmed = ref.trim();
3
+ if (trimmed.length === 0)
4
+ return "";
5
+ const withoutHash = trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
6
+ const parts = withoutHash.split("/").filter(Boolean);
7
+ return parts.length > 0 ? parts[parts.length - 1] : withoutHash;
8
+ }
9
+ function stableStringify(value) {
10
+ if (value === null)
11
+ return "null";
12
+ const t = typeof value;
13
+ if (t === "number" || t === "boolean")
14
+ return JSON.stringify(value);
15
+ if (t === "string")
16
+ return JSON.stringify(value);
17
+ if (Array.isArray(value))
18
+ return `[${value.map(stableStringify).join(",")}]`;
19
+ if (t !== "object")
20
+ return JSON.stringify(value);
21
+ const obj = value;
22
+ const keys = Object.keys(obj).sort();
23
+ const entries = keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`);
24
+ return `{${entries.join(",")}}`;
25
+ }
26
+ export function resolveSchemaRef(doc, ref) {
27
+ const name = normalizeSchemaRef(ref);
28
+ if (!name)
29
+ return undefined;
30
+ return doc.schemas?.[name];
31
+ }
32
+ export function registerSchema(doc, name, jsonSchema) {
33
+ if (!doc.schemas) {
34
+ doc.schemas = {};
35
+ }
36
+ const definition = {
37
+ jsonSchema,
38
+ };
39
+ doc.schemas[name] = definition;
40
+ return definition;
41
+ }
42
+ function updateSchemaRefs(doc, mapping) {
43
+ const rest = doc.protocols?.rest;
44
+ const websocket = doc.protocols?.websocket;
45
+ if (rest?.routes) {
46
+ for (const route of rest.routes) {
47
+ for (const params of [route.pathParams, route.queryParams, route.headers]) {
48
+ if (!params)
49
+ continue;
50
+ for (const p of params) {
51
+ if (!p.schemaRef)
52
+ continue;
53
+ const key = normalizeSchemaRef(p.schemaRef);
54
+ const next = mapping[key];
55
+ if (next && next !== key)
56
+ p.schemaRef = next;
57
+ }
58
+ }
59
+ const requestContent = route.requestBody?.content;
60
+ if (requestContent) {
61
+ for (const media of Object.values(requestContent)) {
62
+ if (!media.schemaRef)
63
+ continue;
64
+ const key = normalizeSchemaRef(media.schemaRef);
65
+ const next = mapping[key];
66
+ if (next && next !== key)
67
+ media.schemaRef = next;
68
+ }
69
+ }
70
+ const responses = route.responses;
71
+ if (responses) {
72
+ for (const resp of Object.values(responses)) {
73
+ const respContent = resp.content;
74
+ if (!respContent)
75
+ continue;
76
+ for (const media of Object.values(respContent)) {
77
+ if (!media.schemaRef)
78
+ continue;
79
+ const key = normalizeSchemaRef(media.schemaRef);
80
+ const next = mapping[key];
81
+ if (next && next !== key)
82
+ media.schemaRef = next;
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ if (websocket?.channels) {
89
+ for (const channel of websocket.channels) {
90
+ if (!channel.messages)
91
+ continue;
92
+ for (const message of channel.messages) {
93
+ if (!message.schemaRef)
94
+ continue;
95
+ const key = normalizeSchemaRef(message.schemaRef);
96
+ const next = mapping[key];
97
+ if (next && next !== key)
98
+ message.schemaRef = next;
99
+ }
100
+ }
101
+ }
102
+ }
103
+ export function dedupeSchemas(doc) {
104
+ if (!doc.schemas)
105
+ return;
106
+ const names = Object.keys(doc.schemas);
107
+ const hashToName = new Map();
108
+ const renameMap = {};
109
+ for (const name of names) {
110
+ const schema = doc.schemas[name];
111
+ const hash = stableStringify(schema?.jsonSchema ?? null);
112
+ const existing = hashToName.get(hash);
113
+ if (!existing) {
114
+ hashToName.set(hash, name);
115
+ continue;
116
+ }
117
+ renameMap[name] = existing;
118
+ }
119
+ const duplicates = Object.keys(renameMap);
120
+ if (duplicates.length === 0)
121
+ return;
122
+ updateSchemaRefs(doc, renameMap);
123
+ for (const dup of duplicates) {
124
+ delete doc.schemas[dup];
125
+ }
126
+ }
@@ -1,11 +1,23 @@
1
+ import Ajv2020 from "ajv/dist/2020.js";
1
2
  import { UniSpecDocument, UniSpecTestsDocument, ValidationResult } from "../types";
3
+ export interface SchemaProvider {
4
+ getSchema: (schemaPath: string) => Promise<object>;
5
+ }
6
+ export interface ValidatorSchemas {
7
+ unispec: object;
8
+ unispecTests?: object;
9
+ subschemas?: object[];
10
+ }
2
11
  export interface ValidateOptions {
12
+ schemas?: ValidatorSchemas;
13
+ schemaProvider?: SchemaProvider;
14
+ ajvOptions?: ConstructorParameters<typeof Ajv2020>[0];
3
15
  }
4
16
  /**
5
17
  * Validate a UniSpec document against the UniSpec JSON Schema.
6
18
  */
7
- export declare function validateUniSpec(doc: UniSpecDocument, _options?: ValidateOptions): Promise<ValidationResult>;
19
+ export declare function validateUniSpec(doc: UniSpecDocument, options?: ValidateOptions): Promise<ValidationResult>;
8
20
  /**
9
21
  * Validate a UniSpec Tests document against the UniSpec Tests JSON Schema.
10
22
  */
11
- export declare function validateUniSpecTests(doc: UniSpecTestsDocument, _options?: ValidateOptions): Promise<ValidationResult>;
23
+ export declare function validateUniSpecTests(doc: UniSpecTestsDocument, options?: ValidateOptions): Promise<ValidationResult>;
@@ -1,30 +1,89 @@
1
1
  import Ajv2020 from "ajv/dist/2020.js";
2
- import fs from "fs";
3
- import path from "path";
4
- import { unispec as unispecSchema, manifest as unispecManifest } from "@unispechq/unispec-schema";
5
- const ajv = new Ajv2020({
6
- allErrors: true,
7
- strict: true,
8
- });
9
- // Register minimal URI format to satisfy UniSpec schemas (service.environments[*].baseUrl)
10
- ajv.addFormat("uri", true);
11
- // Register all UniSpec subschemas so that Ajv can resolve internal $ref links
12
- try {
13
- const schemaDir = path.join(process.cwd(), "node_modules", "@unispechq", "unispec-schema", "schema");
14
- const types = unispecManifest?.types ?? {};
2
+ let cached = null;
3
+ async function tryCreateNodeSchemaProvider() {
4
+ try {
5
+ const fs = await import("node:fs/promises");
6
+ const path = await import("node:path");
7
+ const module = await import("node:module");
8
+ const requireBasePath = typeof __filename === "string" && __filename.length > 0
9
+ ? __filename
10
+ : path.join(process.cwd(), "__unispec_core_require__.js");
11
+ const require = module.createRequire(requireBasePath);
12
+ const schemaIndexPath = require.resolve("@unispechq/unispec-schema/schema");
13
+ const schemaRoot = path.dirname(schemaIndexPath);
14
+ return {
15
+ getSchema: async (schemaPath) => {
16
+ const fullPath = path.join(schemaRoot, schemaPath);
17
+ const json = await fs.readFile(fullPath, "utf8");
18
+ return JSON.parse(json);
19
+ },
20
+ };
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ async function loadDefaultSchemas() {
27
+ const schemaProvider = await tryCreateNodeSchemaProvider();
28
+ if (!schemaProvider) {
29
+ throw new Error("UniSpec validator: default schema loading is not available in this environment. " +
30
+ "Provide ValidateOptions.schemas (recommended for browsers/edge runtimes) or ValidateOptions.schemaProvider.");
31
+ }
32
+ const { manifest, unispec } = (await import("@unispechq/unispec-schema"));
33
+ const types = manifest?.types ?? {};
15
34
  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);
35
+ const subschemas = await Promise.all(typeSchemaPaths.map(async (relPath) => {
36
+ try {
37
+ return await schemaProvider.getSchema(relPath);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }));
43
+ let unispecTests;
44
+ try {
45
+ unispecTests = await schemaProvider.getSchema("unispec-tests.schema.json");
46
+ }
47
+ catch {
48
+ unispecTests = undefined;
49
+ }
50
+ return {
51
+ unispec: unispec,
52
+ unispecTests,
53
+ subschemas: subschemas.filter((s) => Boolean(s)),
54
+ };
21
55
  }
22
- catch {
23
- // If subschemas cannot be loaded for some reason, validation will still work for
24
- // parts of the schema that do not rely on those $ref references.
56
+ function createAjv(options) {
57
+ const ajv = new Ajv2020({
58
+ allErrors: true,
59
+ strict: true,
60
+ ...(options.ajvOptions ?? {}),
61
+ });
62
+ ajv.addFormat("uri", true);
63
+ return ajv;
64
+ }
65
+ async function getValidator(options = {}) {
66
+ if (cached && !options.schemas && !options.schemaProvider && !options.ajvOptions)
67
+ return cached;
68
+ const schemas = options.schemas ?? (await loadDefaultSchemas());
69
+ const ajv = createAjv(options);
70
+ if (schemas.subschemas?.length) {
71
+ ajv.addSchema(schemas.subschemas);
72
+ }
73
+ const validateUniSpecFn = ajv.compile(schemas.unispec);
74
+ let validateUniSpecTestsFn;
75
+ if (schemas.unispecTests) {
76
+ validateUniSpecTestsFn = ajv.compile(schemas.unispecTests);
77
+ }
78
+ const result = {
79
+ validateUniSpecFn,
80
+ validateUniSpecTestsFn,
81
+ };
82
+ if (!options.schemas && !options.schemaProvider && !options.ajvOptions) {
83
+ cached = result;
84
+ }
85
+ return result;
25
86
  }
26
- const validateFn = ajv.compile(unispecSchema);
27
- let validateTestsFn;
28
87
  function mapAjvErrors(errors) {
29
88
  if (!errors)
30
89
  return [];
@@ -37,7 +96,8 @@ function mapAjvErrors(errors) {
37
96
  /**
38
97
  * Validate a UniSpec document against the UniSpec JSON Schema.
39
98
  */
40
- export async function validateUniSpec(doc, _options = {}) {
99
+ export async function validateUniSpec(doc, options = {}) {
100
+ const { validateUniSpecFn } = await getValidator(options);
41
101
  const docForValidation = {
42
102
  unispecVersion: "0.0.0",
43
103
  service: {
@@ -49,7 +109,7 @@ export async function validateUniSpec(doc, _options = {}) {
49
109
  },
50
110
  extensions: doc.extensions,
51
111
  };
52
- const valid = validateFn(docForValidation);
112
+ const valid = validateUniSpecFn(docForValidation);
53
113
  if (valid) {
54
114
  return {
55
115
  valid: true,
@@ -58,20 +118,27 @@ export async function validateUniSpec(doc, _options = {}) {
58
118
  }
59
119
  return {
60
120
  valid: false,
61
- errors: mapAjvErrors(validateFn.errors),
121
+ errors: mapAjvErrors(validateUniSpecFn.errors),
62
122
  };
63
123
  }
64
124
  /**
65
125
  * Validate a UniSpec Tests document against the UniSpec Tests JSON Schema.
66
126
  */
67
- export async function validateUniSpecTests(doc, _options = {}) {
68
- if (!validateTestsFn) {
69
- const schemaDir = path.join(process.cwd(), "node_modules", "@unispechq", "unispec-schema", "schema");
70
- const testsSchemaPath = path.join(schemaDir, "unispec-tests.schema.json");
71
- const testsSchema = JSON.parse(fs.readFileSync(testsSchemaPath, "utf8"));
72
- validateTestsFn = ajv.compile(testsSchema);
127
+ export async function validateUniSpecTests(doc, options = {}) {
128
+ const { validateUniSpecTestsFn } = await getValidator(options);
129
+ if (!validateUniSpecTestsFn) {
130
+ return {
131
+ valid: false,
132
+ errors: [
133
+ {
134
+ message: "UniSpec tests validation schema is not available. Provide ValidateOptions.schemas.unispecTests or a schemaProvider that can load it.",
135
+ path: "",
136
+ code: "schema_missing",
137
+ },
138
+ ],
139
+ };
73
140
  }
74
- const valid = validateTestsFn(doc);
141
+ const valid = validateUniSpecTestsFn(doc);
75
142
  if (valid) {
76
143
  return {
77
144
  valid: true,
@@ -80,6 +147,6 @@ export async function validateUniSpecTests(doc, _options = {}) {
80
147
  }
81
148
  return {
82
149
  valid: false,
83
- errors: mapAjvErrors(validateTestsFn.errors),
150
+ errors: mapAjvErrors(validateUniSpecTestsFn.errors),
84
151
  };
85
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unispechq/unispec-core",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Central UniSpec Core Engine providing parsing, validation, normalization, diffing, and conversion of UniSpec specs.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,7 +48,7 @@
48
48
  "release:major": "node scripts/release.js major"
49
49
  },
50
50
  "dependencies": {
51
- "@unispechq/unispec-schema": "^0.3.3",
51
+ "@unispechq/unispec-schema": "^0.3.4",
52
52
  "ajv": "^8.12.0"
53
53
  },
54
54
  "devDependencies": {