@kravc/schema 2.8.0-alpha.0 → 2.8.0-alpha.1

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.
@@ -0,0 +1,67 @@
1
+ import Schema from '../Schema';
2
+ /**
3
+ * Creates a map of schemas by ID, loading from YAML files and merging with programmatic schemas.
4
+ *
5
+ * **Intent:** Build a centralized schema registry that combines file-based YAML schemas
6
+ * with programmatically created Schema instances, providing a unified lookup mechanism
7
+ * by schema ID. This enables hybrid schema management where some schemas are defined
8
+ * in YAML files while others are created dynamically in code.
9
+ *
10
+ * **Use Cases:**
11
+ * - Initialize schema registries for validators from both files and code
12
+ * - Support schema composition where base schemas come from files and extended
13
+ * schemas are created programmatically
14
+ * - Enable schema overriding where programmatic schemas can replace file-based ones
15
+ * - Build schema maps for credential factories or API validation systems
16
+ * - Support development workflows where schemas evolve from YAML to code
17
+ *
18
+ * @param servicePath - Path to directory containing YAML schema files (searched recursively)
19
+ * @param modules - Array of Schema instances or other values (non-Schema values are filtered out)
20
+ * @returns Record mapping schema IDs to Schema instances, with modules schemas overriding YAML schemas
21
+ *
22
+ * **Example - Basic Usage:**
23
+ * ```typescript
24
+ * const schemasMap = createSchemasMap('/path/to/examples/schemas', []);
25
+ * // Returns: {
26
+ * // FavoriteItem: Schema { id: 'FavoriteItem', ... },
27
+ * // Profile: Schema { id: 'Profile', ... },
28
+ * // Status: Schema { id: 'Status', ... },
29
+ * // Preferences: Schema { id: 'Preferences', ... }
30
+ * // }
31
+ * ```
32
+ *
33
+ * **Example - Merging Programmatic Schemas:**
34
+ * ```typescript
35
+ * const customSchema = new Schema(
36
+ * { customField: { type: 'string' } },
37
+ * 'CustomSchema'
38
+ * );
39
+ * const schemasMap = createSchemasMap('/path/to/schemas', [customSchema]);
40
+ * // schemasMap contains both YAML schemas and CustomSchema
41
+ * ```
42
+ *
43
+ * **Example - Overriding YAML Schemas:**
44
+ * ```typescript
45
+ * const updatedProfile = new Schema(
46
+ * { name: { type: 'string' }, newField: { type: 'number' } },
47
+ * 'Profile'
48
+ * );
49
+ * const schemasMap = createSchemasMap('/path/to/schemas', [updatedProfile]);
50
+ * // schemasMap.Profile is the updatedProfile instance, not the YAML version
51
+ * ```
52
+ *
53
+ * **Example - Filtering Non-Schema Values:**
54
+ * ```typescript
55
+ * const schema = new Schema({ field: { type: 'string' } }, 'Test');
56
+ * const schemasMap = createSchemasMap('/path/to/schemas', [
57
+ * schema,
58
+ * 'not a schema',
59
+ * { id: 'fake' },
60
+ * null
61
+ * ]);
62
+ * // Only the Schema instance is included, other values are ignored
63
+ * ```
64
+ */
65
+ declare const createSchemasMap: (servicePath: string, modules: unknown[]) => Record<string, Schema>;
66
+ export default createSchemasMap;
67
+ //# sourceMappingURL=createSchemasMap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createSchemasMap.d.ts","sourceRoot":"","sources":["../../src/helpers/createSchemasMap.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,WAAW,CAAC;AAqI/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,QAAA,MAAM,gBAAgB,GAAI,aAAa,MAAM,EAAE,SAAS,OAAO,EAAE,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAYxF,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const path_1 = __importDefault(require("path"));
7
+ const Schema_1 = __importDefault(require("../Schema"));
8
+ const js_yaml_1 = require("js-yaml");
9
+ const lodash_1 = require("lodash");
10
+ const fs_1 = require("fs");
11
+ /**
12
+ * Reads schema source from YAML file and returns a Schema instance.
13
+ *
14
+ * **Intent:** Load and parse a single YAML schema file, extracting the schema ID
15
+ * from the filename and creating a Schema instance for use in validation or
16
+ * schema composition.
17
+ *
18
+ * **Use Cases:**
19
+ * - Load individual schema files during application startup
20
+ * - Parse YAML schema definitions into Schema instances
21
+ * - Extract schema identifiers from file paths automatically
22
+ * - Support both enum and properties-based schemas from YAML files
23
+ *
24
+ * @param yamlPath - Absolute or relative path to the YAML schema file
25
+ * @returns A Schema instance with ID extracted from the filename (without .yaml extension)
26
+ *
27
+ * **Example:**
28
+ * ```typescript
29
+ * const schema = loadSync('/path/to/schemas/User.yaml');
30
+ * // schema.id === 'User'
31
+ * // schema.source contains the parsed YAML content
32
+ * ```
33
+ *
34
+ * **Example - Enum Schema:**
35
+ * ```typescript
36
+ * const statusSchema = loadSync('/path/to/schemas/Status.yaml');
37
+ * // If Status.yaml contains: { enum: ['PENDING', 'ACTIVE'] }
38
+ * // statusSchema.isEnum === true
39
+ * ```
40
+ */
41
+ const loadSync = (yamlPath) => {
42
+ const schemaId = yamlPath
43
+ .split('/')
44
+ .reverse()[0]
45
+ .split('.yaml')[0];
46
+ const file = (0, fs_1.readFileSync)(yamlPath);
47
+ const source = (0, js_yaml_1.load)(file.toString());
48
+ return new Schema_1.default(source, schemaId);
49
+ };
50
+ /**
51
+ * Recursively lists all files in a directory and its subdirectories.
52
+ *
53
+ * **Intent:** Traverse a directory tree and collect all file paths, enabling
54
+ * discovery of schema files nested in subdirectories without manual path specification.
55
+ *
56
+ * **Use Cases:**
57
+ * - Find all schema files in a directory structure
58
+ * - Support organized schema layouts with nested folders
59
+ * - Enable schema discovery without hardcoding file paths
60
+ * - Prepare file list for filtering and processing
61
+ *
62
+ * @param servicePath - Path to the directory to traverse
63
+ * @returns Array of absolute file paths found in the directory tree
64
+ *
65
+ * **Example:**
66
+ * ```typescript
67
+ * const files = listFilesSync('/path/to/schemas');
68
+ * // Returns: [
69
+ * // '/path/to/schemas/User.yaml',
70
+ * // '/path/to/schemas/nested/Profile.yaml',
71
+ * // '/path/to/schemas/nested/deep/Status.yaml'
72
+ * // ]
73
+ * ```
74
+ *
75
+ * **Example - Flat Directory:**
76
+ * ```typescript
77
+ * const files = listFilesSync('/path/to/schemas');
78
+ * // Returns: [
79
+ * // '/path/to/schemas/User.yaml',
80
+ * // '/path/to/schemas/Profile.yaml'
81
+ * // ]
82
+ * ```
83
+ */
84
+ const listFilesSync = (servicePath) => (0, fs_1.readdirSync)(servicePath)
85
+ .reduce((filePaths, fileName) => (0, fs_1.statSync)(path_1.default.join(servicePath, fileName)).isDirectory() ?
86
+ filePaths.concat(listFilesSync(path_1.default.join(servicePath, fileName))) :
87
+ filePaths.concat(path_1.default.join(servicePath, fileName)), []);
88
+ /**
89
+ * Reads all YAML schema files from a directory and creates Schema instances.
90
+ *
91
+ * **Intent:** Bulk load schema definitions from YAML files in a directory,
92
+ * automatically discovering and parsing all schema files for use in schema
93
+ * registries or validators.
94
+ *
95
+ * **Use Cases:**
96
+ * - Load all schemas from a schemas directory at application startup
97
+ * - Initialize schema registries from file-based definitions
98
+ * - Support schema-as-code workflows where schemas are stored as YAML files
99
+ * - Enable automatic schema discovery without manual registration
100
+ *
101
+ * @param servicePath - Path to the directory containing YAML schema files
102
+ * @returns Array of Schema instances, one for each YAML file found
103
+ *
104
+ * **Example:**
105
+ * ```typescript
106
+ * const schemas = readSchemasSync('/path/to/examples/schemas');
107
+ * // Returns: [
108
+ * // Schema { id: 'FavoriteItem', ... },
109
+ * // Schema { id: 'Profile', ... },
110
+ * // Schema { id: 'Status', ... },
111
+ * // Schema { id: 'Preferences', ... }
112
+ * // ]
113
+ * ```
114
+ *
115
+ * **Example - With Nested Directories:**
116
+ * ```typescript
117
+ * const schemas = readSchemasSync('/path/to/schemas');
118
+ * // Automatically finds schemas in subdirectories:
119
+ * // - /path/to/schemas/User.yaml
120
+ * // - /path/to/schemas/nested/Profile.yaml
121
+ * ```
122
+ */
123
+ const readSchemasSync = (servicePath) => listFilesSync(servicePath)
124
+ .filter((fileName) => fileName.endsWith('.yaml'))
125
+ .map((schemaPath) => loadSync(schemaPath));
126
+ /**
127
+ * Creates a map of schemas by ID, loading from YAML files and merging with programmatic schemas.
128
+ *
129
+ * **Intent:** Build a centralized schema registry that combines file-based YAML schemas
130
+ * with programmatically created Schema instances, providing a unified lookup mechanism
131
+ * by schema ID. This enables hybrid schema management where some schemas are defined
132
+ * in YAML files while others are created dynamically in code.
133
+ *
134
+ * **Use Cases:**
135
+ * - Initialize schema registries for validators from both files and code
136
+ * - Support schema composition where base schemas come from files and extended
137
+ * schemas are created programmatically
138
+ * - Enable schema overriding where programmatic schemas can replace file-based ones
139
+ * - Build schema maps for credential factories or API validation systems
140
+ * - Support development workflows where schemas evolve from YAML to code
141
+ *
142
+ * @param servicePath - Path to directory containing YAML schema files (searched recursively)
143
+ * @param modules - Array of Schema instances or other values (non-Schema values are filtered out)
144
+ * @returns Record mapping schema IDs to Schema instances, with modules schemas overriding YAML schemas
145
+ *
146
+ * **Example - Basic Usage:**
147
+ * ```typescript
148
+ * const schemasMap = createSchemasMap('/path/to/examples/schemas', []);
149
+ * // Returns: {
150
+ * // FavoriteItem: Schema { id: 'FavoriteItem', ... },
151
+ * // Profile: Schema { id: 'Profile', ... },
152
+ * // Status: Schema { id: 'Status', ... },
153
+ * // Preferences: Schema { id: 'Preferences', ... }
154
+ * // }
155
+ * ```
156
+ *
157
+ * **Example - Merging Programmatic Schemas:**
158
+ * ```typescript
159
+ * const customSchema = new Schema(
160
+ * { customField: { type: 'string' } },
161
+ * 'CustomSchema'
162
+ * );
163
+ * const schemasMap = createSchemasMap('/path/to/schemas', [customSchema]);
164
+ * // schemasMap contains both YAML schemas and CustomSchema
165
+ * ```
166
+ *
167
+ * **Example - Overriding YAML Schemas:**
168
+ * ```typescript
169
+ * const updatedProfile = new Schema(
170
+ * { name: { type: 'string' }, newField: { type: 'number' } },
171
+ * 'Profile'
172
+ * );
173
+ * const schemasMap = createSchemasMap('/path/to/schemas', [updatedProfile]);
174
+ * // schemasMap.Profile is the updatedProfile instance, not the YAML version
175
+ * ```
176
+ *
177
+ * **Example - Filtering Non-Schema Values:**
178
+ * ```typescript
179
+ * const schema = new Schema({ field: { type: 'string' } }, 'Test');
180
+ * const schemasMap = createSchemasMap('/path/to/schemas', [
181
+ * schema,
182
+ * 'not a schema',
183
+ * { id: 'fake' },
184
+ * null
185
+ * ]);
186
+ * // Only the Schema instance is included, other values are ignored
187
+ * ```
188
+ */
189
+ const createSchemasMap = (servicePath, modules) => {
190
+ const yamlSchemas = readSchemasSync(servicePath);
191
+ const schemasMap = (0, lodash_1.keyBy)(yamlSchemas, 'id');
192
+ const schemas = modules
193
+ .filter(schema => schema instanceof Schema_1.default);
194
+ for (const schema of schemas) {
195
+ schemasMap[schema.id] = schema;
196
+ }
197
+ return schemasMap;
198
+ };
199
+ exports.default = createSchemasMap;
200
+ //# sourceMappingURL=createSchemasMap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createSchemasMap.js","sourceRoot":"","sources":["../../src/helpers/createSchemasMap.ts"],"names":[],"mappings":";;;;;AAAA,gDAAwB;AACxB,uDAA+B;AAC/B,qCAA+B;AAC/B,mCAA+B;AAE/B,2BAAyD;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,QAAQ,GAAG,CAAC,QAAgB,EAAE,EAAE;IACpC,MAAM,QAAQ,GAAG,QAAQ;SACtB,KAAK,CAAC,GAAG,CAAC;SACV,OAAO,EAAE,CAAC,CAAC,CAAC;SACZ,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAErB,MAAM,IAAI,GAAG,IAAA,iBAAY,EAAC,QAAQ,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,IAAA,cAAI,EAAC,IAAI,CAAC,QAAQ,EAAE,CAAkC,CAAC;IAEtE,OAAO,IAAI,gBAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AACtC,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,aAAa,GAAG,CAAC,WAAmB,EAAY,EAAE,CACtD,IAAA,gBAAW,EAAC,WAAW,CAAC;KACrB,MAAM,CACL,CAAC,SAAmB,EAAE,QAAgB,EAAE,EAAE,CACxC,IAAA,aAAQ,EACN,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAC/C,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,SAAS,CAAC,MAAM,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CACpD,EACH,EAAE,CAAC,CAAC;AAEV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,eAAe,GAAG,CAAC,WAAmB,EAAE,EAAE,CAC9C,aAAa,CAAC,WAAW,CAAC;KACvB,MAAM,CAAC,CAAC,QAAgB,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;KACxD,GAAG,CAAC,CAAC,UAAkB,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,MAAM,gBAAgB,GAAG,CAAC,WAAmB,EAAE,OAAkB,EAA0B,EAAE;IAC3F,MAAM,WAAW,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,IAAA,cAAK,EAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAE5C,MAAM,OAAO,GAAG,OAAO;SACpB,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,YAAY,gBAAM,CAAC,CAAC;IAE9C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC;IACjC,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC,CAAC;AAEF,kBAAe,gBAAgB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import got from './helpers/got';
1
2
  import Schema from './Schema';
2
3
  import Validator from './Validator';
3
4
  import documentLoader from './ld/documentLoader';
4
5
  import ValidationError from './ValidationError';
6
+ import createSchemasMap from './helpers/createSchemasMap';
5
7
  import CredentialFactory from './CredentialFactory';
6
- export { Schema, Validator, documentLoader, ValidationError, CredentialFactory };
8
+ export { got, Schema, Validator, documentLoader, ValidationError, createSchemasMap, CredentialFactory };
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,UAAU,CAAC;AAC9B,OAAO,SAAS,MAAM,aAAa,CAAC;AACpC,OAAO,cAAc,MAAM,qBAAqB,CAAC;AACjD,OAAO,eAAe,MAAM,mBAAmB,CAAC;AAChD,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EACL,MAAM,EACN,SAAS,EACT,cAAc,EACd,eAAe,EACf,iBAAiB,EAClB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,eAAe,CAAC;AAChC,OAAO,MAAM,MAAM,UAAU,CAAC;AAC9B,OAAO,SAAS,MAAM,aAAa,CAAC;AACpC,OAAO,cAAc,MAAM,qBAAqB,CAAC;AACjD,OAAO,eAAe,MAAM,mBAAmB,CAAC;AAChD,OAAO,gBAAgB,MAAM,4BAA4B,CAAC;AAC1D,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EACL,GAAG,EACH,MAAM,EACN,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EAClB,CAAC"}
package/dist/index.js CHANGED
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CredentialFactory = exports.ValidationError = exports.documentLoader = exports.Validator = exports.Schema = void 0;
6
+ exports.CredentialFactory = exports.createSchemasMap = exports.ValidationError = exports.documentLoader = exports.Validator = exports.Schema = exports.got = void 0;
7
+ const got_1 = __importDefault(require("./helpers/got"));
8
+ exports.got = got_1.default;
7
9
  const Schema_1 = __importDefault(require("./Schema"));
8
10
  exports.Schema = Schema_1.default;
9
11
  const Validator_1 = __importDefault(require("./Validator"));
@@ -12,6 +14,8 @@ const documentLoader_1 = __importDefault(require("./ld/documentLoader"));
12
14
  exports.documentLoader = documentLoader_1.default;
13
15
  const ValidationError_1 = __importDefault(require("./ValidationError"));
14
16
  exports.ValidationError = ValidationError_1.default;
17
+ const createSchemasMap_1 = __importDefault(require("./helpers/createSchemasMap"));
18
+ exports.createSchemasMap = createSchemasMap_1.default;
15
19
  const CredentialFactory_1 = __importDefault(require("./CredentialFactory"));
16
20
  exports.CredentialFactory = CredentialFactory_1.default;
17
21
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,sDAA8B;AAO5B,iBAPK,gBAAM,CAOL;AANR,4DAAoC;AAOlC,oBAPK,mBAAS,CAOL;AANX,yEAAiD;AAO/C,yBAPK,wBAAc,CAOL;AANhB,wEAAgD;AAO9C,0BAPK,yBAAe,CAOL;AANjB,4EAAoD;AAOlD,4BAPK,2BAAiB,CAOL"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,wDAAgC;AAS9B,cATK,aAAG,CASL;AARL,sDAA8B;AAS5B,iBATK,gBAAM,CASL;AARR,4DAAoC;AASlC,oBATK,mBAAS,CASL;AARX,yEAAiD;AAS/C,yBATK,wBAAc,CASL;AARhB,wEAAgD;AAS9C,0BATK,yBAAe,CASL;AARjB,kFAA0D;AASxD,2BATK,0BAAgB,CASL;AARlB,4EAAoD;AASlD,4BATK,2BAAiB,CASL"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kravc/schema",
3
- "version": "2.8.0-alpha.0",
3
+ "version": "2.8.0-alpha.1",
4
4
  "description": "Advanced JSON schema manipulation and validation library.",
5
5
  "keywords": [
6
6
  "JSON",
@@ -0,0 +1,238 @@
1
+ import path from 'path';
2
+ import createSchemasMap from '../createSchemasMap';
3
+ import Schema from '../../Schema';
4
+
5
+ describe('createSchemasMap', () => {
6
+ const examplesSchemasPath = path.join(__dirname, '../../../examples/schemas');
7
+
8
+ describe('loading schemas from YAML files', () => {
9
+ it('should load all YAML schemas from the examples/schemas directory', () => {
10
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
11
+
12
+ expect(Object.keys(schemasMap).length).toBeGreaterThan(0);
13
+ expect(schemasMap).toHaveProperty('FavoriteItem');
14
+ expect(schemasMap).toHaveProperty('Profile');
15
+ expect(schemasMap).toHaveProperty('Status');
16
+ expect(schemasMap).toHaveProperty('Preferences');
17
+ });
18
+
19
+ it('should create Schema instances from YAML files', () => {
20
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
21
+
22
+ expect(schemasMap.FavoriteItem).toBeInstanceOf(Schema);
23
+ expect(schemasMap.Profile).toBeInstanceOf(Schema);
24
+ expect(schemasMap.Status).toBeInstanceOf(Schema);
25
+ expect(schemasMap.Preferences).toBeInstanceOf(Schema);
26
+ });
27
+
28
+ it('should extract schema ID from YAML filename', () => {
29
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
30
+
31
+ expect(schemasMap.FavoriteItem.id).toBe('FavoriteItem');
32
+ expect(schemasMap.Profile.id).toBe('Profile');
33
+ expect(schemasMap.Status.id).toBe('Status');
34
+ expect(schemasMap.Preferences.id).toBe('Preferences');
35
+ });
36
+
37
+ it('should handle enum schemas correctly', () => {
38
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
39
+
40
+ expect(schemasMap.Status).toBeInstanceOf(Schema);
41
+ expect(schemasMap.Status.isEnum).toBe(true);
42
+ });
43
+
44
+ it('should handle properties schemas correctly', () => {
45
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
46
+
47
+ expect(schemasMap.Profile).toBeInstanceOf(Schema);
48
+ expect(schemasMap.Profile.isEnum).toBe(false);
49
+ expect(schemasMap.FavoriteItem).toBeInstanceOf(Schema);
50
+ expect(schemasMap.FavoriteItem.isEnum).toBe(false);
51
+ });
52
+ });
53
+
54
+ describe('merging modules array', () => {
55
+ it('should merge Schema instances from modules array', () => {
56
+ const customSchema = new Schema(
57
+ { customField: { type: 'string' } },
58
+ 'CustomSchema'
59
+ );
60
+
61
+ const schemasMap = createSchemasMap(examplesSchemasPath, [customSchema]);
62
+
63
+ expect(schemasMap.CustomSchema).toBeInstanceOf(Schema);
64
+ expect(schemasMap.CustomSchema.id).toBe('CustomSchema');
65
+ });
66
+
67
+ it('should override YAML schemas with modules schemas if same ID', () => {
68
+ const customProfile = new Schema(
69
+ { customField: { type: 'string' } },
70
+ 'Profile'
71
+ );
72
+
73
+ const schemasMap = createSchemasMap(examplesSchemasPath, [customProfile]);
74
+
75
+ expect(schemasMap.Profile).toBeInstanceOf(Schema);
76
+ expect(schemasMap.Profile.id).toBe('Profile');
77
+ // The custom schema should override the YAML one
78
+ const source = schemasMap.Profile.source;
79
+ expect(source).toHaveProperty('customField');
80
+ });
81
+
82
+ it('should filter out non-Schema instances from modules', () => {
83
+ const customSchema = new Schema(
84
+ { customField: { type: 'string' } },
85
+ 'CustomSchema'
86
+ );
87
+ const notASchema = { id: 'NotASchema', someProperty: 'value' };
88
+ const alsoNotASchema = 'string value';
89
+
90
+ const schemasMap = createSchemasMap(examplesSchemasPath, [
91
+ customSchema,
92
+ notASchema,
93
+ alsoNotASchema,
94
+ null,
95
+ undefined,
96
+ ]);
97
+
98
+ expect(schemasMap.CustomSchema).toBeInstanceOf(Schema);
99
+ expect(schemasMap).not.toHaveProperty('NotASchema');
100
+ });
101
+
102
+ it('should handle empty modules array', () => {
103
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
104
+
105
+ expect(Object.keys(schemasMap).length).toBeGreaterThan(0);
106
+ expect(schemasMap.FavoriteItem).toBeInstanceOf(Schema);
107
+ });
108
+
109
+ it('should merge multiple Schema instances from modules', () => {
110
+ const schema1 = new Schema({ field1: { type: 'string' } }, 'Schema1');
111
+ const schema2 = new Schema({ field2: { type: 'number' } }, 'Schema2');
112
+ const schema3 = new Schema({ field3: { type: 'boolean' } }, 'Schema3');
113
+
114
+ const schemasMap = createSchemasMap(examplesSchemasPath, [
115
+ schema1,
116
+ schema2,
117
+ schema3,
118
+ ]);
119
+
120
+ expect(schemasMap.Schema1).toBeInstanceOf(Schema);
121
+ expect(schemasMap.Schema2).toBeInstanceOf(Schema);
122
+ expect(schemasMap.Schema3).toBeInstanceOf(Schema);
123
+ expect(schemasMap.Schema1.id).toBe('Schema1');
124
+ expect(schemasMap.Schema2.id).toBe('Schema2');
125
+ expect(schemasMap.Schema3.id).toBe('Schema3');
126
+ });
127
+ });
128
+
129
+ describe('return value structure', () => {
130
+ it('should return a Record<string, Schema>', () => {
131
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
132
+
133
+ expect(typeof schemasMap).toBe('object');
134
+ expect(schemasMap).not.toBeNull();
135
+ expect(Array.isArray(schemasMap)).toBe(false);
136
+
137
+ // Check that all values are Schema instances
138
+ Object.values(schemasMap).forEach((schema) => {
139
+ expect(schema).toBeInstanceOf(Schema);
140
+ });
141
+ });
142
+
143
+ it('should use schema ID as keys in the map', () => {
144
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
145
+
146
+ Object.entries(schemasMap).forEach(([key, schema]) => {
147
+ expect(schema.id).toBe(key);
148
+ });
149
+ });
150
+ });
151
+
152
+ describe('edge cases', () => {
153
+ it('should handle empty directory gracefully', () => {
154
+ const emptyDir = path.join(__dirname, '../../../examples');
155
+ // This directory might have subdirectories but no YAML files directly
156
+ // We'll test with a path that exists but may have no YAML files
157
+ const schemasMap = createSchemasMap(emptyDir, []);
158
+
159
+ expect(typeof schemasMap).toBe('object');
160
+ // If there are no YAML files, the map should be empty or contain only what's in modules
161
+ });
162
+
163
+ it('should ignore non-YAML files', () => {
164
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
165
+
166
+ // Should only contain schemas from YAML files, not TypeScript files
167
+ Object.keys(schemasMap).forEach((key) => {
168
+ expect(key).not.toBe('FavoriteItemSchema');
169
+ expect(key).not.toBe('ProfileSchema');
170
+ expect(key).not.toBe('PreferencesSchema');
171
+ expect(key).not.toBe('StatusSchema');
172
+ });
173
+ });
174
+
175
+ it('should handle modules array with only non-Schema values', () => {
176
+ const schemasMap = createSchemasMap(examplesSchemasPath, [
177
+ 'string',
178
+ 123,
179
+ {},
180
+ null,
181
+ undefined,
182
+ ]);
183
+
184
+ // Should still have YAML schemas
185
+ expect(schemasMap.FavoriteItem).toBeInstanceOf(Schema);
186
+ expect(schemasMap.Profile).toBeInstanceOf(Schema);
187
+ });
188
+ });
189
+
190
+ describe('schema content validation', () => {
191
+ it('should correctly parse FavoriteItem schema', () => {
192
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
193
+ const favoriteItem = schemasMap.FavoriteItem;
194
+
195
+ expect(favoriteItem).toBeInstanceOf(Schema);
196
+ expect(favoriteItem.isEnum).toBe(false);
197
+ const source = favoriteItem.source;
198
+ expect(source).toHaveProperty('id');
199
+ expect(source).toHaveProperty('name');
200
+ expect(source).toHaveProperty('status');
201
+ });
202
+
203
+ it('should correctly parse Profile schema', () => {
204
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
205
+ const profile = schemasMap.Profile;
206
+
207
+ expect(profile).toBeInstanceOf(Schema);
208
+ expect(profile.isEnum).toBe(false);
209
+ const source = profile.source;
210
+ expect(source).toHaveProperty('name');
211
+ expect(source).toHaveProperty('status');
212
+ expect(source).toHaveProperty('gender');
213
+ });
214
+
215
+ it('should correctly parse Status enum schema', () => {
216
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
217
+ const status = schemasMap.Status;
218
+
219
+ expect(status).toBeInstanceOf(Schema);
220
+ expect(status.isEnum).toBe(true);
221
+ const source = status.source;
222
+ expect(source).toHaveProperty('enum');
223
+ expect(Array.isArray(source.enum)).toBe(true);
224
+ });
225
+
226
+ it('should correctly parse Preferences schema', () => {
227
+ const schemasMap = createSchemasMap(examplesSchemasPath, []);
228
+ const preferences = schemasMap.Preferences;
229
+
230
+ expect(preferences).toBeInstanceOf(Schema);
231
+ expect(preferences.isEnum).toBe(false);
232
+ const source = preferences.source;
233
+ expect(source).toHaveProperty('age');
234
+ expect(source).toHaveProperty('height');
235
+ expect(source).toHaveProperty('isNotificationEnabled');
236
+ });
237
+ });
238
+ });
@@ -0,0 +1,212 @@
1
+ import path from 'path';
2
+ import Schema from '../Schema';
3
+ import { load } from 'js-yaml';
4
+ import { keyBy } from 'lodash';
5
+ import type { EnumSchema, PropertiesSchema } from './JsonSchema';
6
+ import { readFileSync, readdirSync, statSync } from 'fs';
7
+
8
+ /**
9
+ * Reads schema source from YAML file and returns a Schema instance.
10
+ *
11
+ * **Intent:** Load and parse a single YAML schema file, extracting the schema ID
12
+ * from the filename and creating a Schema instance for use in validation or
13
+ * schema composition.
14
+ *
15
+ * **Use Cases:**
16
+ * - Load individual schema files during application startup
17
+ * - Parse YAML schema definitions into Schema instances
18
+ * - Extract schema identifiers from file paths automatically
19
+ * - Support both enum and properties-based schemas from YAML files
20
+ *
21
+ * @param yamlPath - Absolute or relative path to the YAML schema file
22
+ * @returns A Schema instance with ID extracted from the filename (without .yaml extension)
23
+ *
24
+ * **Example:**
25
+ * ```typescript
26
+ * const schema = loadSync('/path/to/schemas/User.yaml');
27
+ * // schema.id === 'User'
28
+ * // schema.source contains the parsed YAML content
29
+ * ```
30
+ *
31
+ * **Example - Enum Schema:**
32
+ * ```typescript
33
+ * const statusSchema = loadSync('/path/to/schemas/Status.yaml');
34
+ * // If Status.yaml contains: { enum: ['PENDING', 'ACTIVE'] }
35
+ * // statusSchema.isEnum === true
36
+ * ```
37
+ */
38
+ const loadSync = (yamlPath: string) => {
39
+ const schemaId = yamlPath
40
+ .split('/')
41
+ .reverse()[0]
42
+ .split('.yaml')[0];
43
+
44
+ const file = readFileSync(yamlPath);
45
+ const source = load(file.toString()) as EnumSchema | PropertiesSchema;
46
+
47
+ return new Schema(source, schemaId);
48
+ };
49
+
50
+ /**
51
+ * Recursively lists all files in a directory and its subdirectories.
52
+ *
53
+ * **Intent:** Traverse a directory tree and collect all file paths, enabling
54
+ * discovery of schema files nested in subdirectories without manual path specification.
55
+ *
56
+ * **Use Cases:**
57
+ * - Find all schema files in a directory structure
58
+ * - Support organized schema layouts with nested folders
59
+ * - Enable schema discovery without hardcoding file paths
60
+ * - Prepare file list for filtering and processing
61
+ *
62
+ * @param servicePath - Path to the directory to traverse
63
+ * @returns Array of absolute file paths found in the directory tree
64
+ *
65
+ * **Example:**
66
+ * ```typescript
67
+ * const files = listFilesSync('/path/to/schemas');
68
+ * // Returns: [
69
+ * // '/path/to/schemas/User.yaml',
70
+ * // '/path/to/schemas/nested/Profile.yaml',
71
+ * // '/path/to/schemas/nested/deep/Status.yaml'
72
+ * // ]
73
+ * ```
74
+ *
75
+ * **Example - Flat Directory:**
76
+ * ```typescript
77
+ * const files = listFilesSync('/path/to/schemas');
78
+ * // Returns: [
79
+ * // '/path/to/schemas/User.yaml',
80
+ * // '/path/to/schemas/Profile.yaml'
81
+ * // ]
82
+ * ```
83
+ */
84
+ const listFilesSync = (servicePath: string): string[] =>
85
+ readdirSync(servicePath)
86
+ .reduce(
87
+ (filePaths: string[], fileName: string) =>
88
+ statSync(
89
+ path.join(servicePath, fileName)).isDirectory() ?
90
+ filePaths.concat(listFilesSync(path.join(servicePath, fileName))) :
91
+ filePaths.concat(path.join(servicePath, fileName)
92
+ )
93
+ , []);
94
+
95
+ /**
96
+ * Reads all YAML schema files from a directory and creates Schema instances.
97
+ *
98
+ * **Intent:** Bulk load schema definitions from YAML files in a directory,
99
+ * automatically discovering and parsing all schema files for use in schema
100
+ * registries or validators.
101
+ *
102
+ * **Use Cases:**
103
+ * - Load all schemas from a schemas directory at application startup
104
+ * - Initialize schema registries from file-based definitions
105
+ * - Support schema-as-code workflows where schemas are stored as YAML files
106
+ * - Enable automatic schema discovery without manual registration
107
+ *
108
+ * @param servicePath - Path to the directory containing YAML schema files
109
+ * @returns Array of Schema instances, one for each YAML file found
110
+ *
111
+ * **Example:**
112
+ * ```typescript
113
+ * const schemas = readSchemasSync('/path/to/examples/schemas');
114
+ * // Returns: [
115
+ * // Schema { id: 'FavoriteItem', ... },
116
+ * // Schema { id: 'Profile', ... },
117
+ * // Schema { id: 'Status', ... },
118
+ * // Schema { id: 'Preferences', ... }
119
+ * // ]
120
+ * ```
121
+ *
122
+ * **Example - With Nested Directories:**
123
+ * ```typescript
124
+ * const schemas = readSchemasSync('/path/to/schemas');
125
+ * // Automatically finds schemas in subdirectories:
126
+ * // - /path/to/schemas/User.yaml
127
+ * // - /path/to/schemas/nested/Profile.yaml
128
+ * ```
129
+ */
130
+ const readSchemasSync = (servicePath: string) =>
131
+ listFilesSync(servicePath)
132
+ .filter((fileName: string) => fileName.endsWith('.yaml'))
133
+ .map((schemaPath: string) => loadSync(schemaPath));
134
+
135
+ /**
136
+ * Creates a map of schemas by ID, loading from YAML files and merging with programmatic schemas.
137
+ *
138
+ * **Intent:** Build a centralized schema registry that combines file-based YAML schemas
139
+ * with programmatically created Schema instances, providing a unified lookup mechanism
140
+ * by schema ID. This enables hybrid schema management where some schemas are defined
141
+ * in YAML files while others are created dynamically in code.
142
+ *
143
+ * **Use Cases:**
144
+ * - Initialize schema registries for validators from both files and code
145
+ * - Support schema composition where base schemas come from files and extended
146
+ * schemas are created programmatically
147
+ * - Enable schema overriding where programmatic schemas can replace file-based ones
148
+ * - Build schema maps for credential factories or API validation systems
149
+ * - Support development workflows where schemas evolve from YAML to code
150
+ *
151
+ * @param servicePath - Path to directory containing YAML schema files (searched recursively)
152
+ * @param modules - Array of Schema instances or other values (non-Schema values are filtered out)
153
+ * @returns Record mapping schema IDs to Schema instances, with modules schemas overriding YAML schemas
154
+ *
155
+ * **Example - Basic Usage:**
156
+ * ```typescript
157
+ * const schemasMap = createSchemasMap('/path/to/examples/schemas', []);
158
+ * // Returns: {
159
+ * // FavoriteItem: Schema { id: 'FavoriteItem', ... },
160
+ * // Profile: Schema { id: 'Profile', ... },
161
+ * // Status: Schema { id: 'Status', ... },
162
+ * // Preferences: Schema { id: 'Preferences', ... }
163
+ * // }
164
+ * ```
165
+ *
166
+ * **Example - Merging Programmatic Schemas:**
167
+ * ```typescript
168
+ * const customSchema = new Schema(
169
+ * { customField: { type: 'string' } },
170
+ * 'CustomSchema'
171
+ * );
172
+ * const schemasMap = createSchemasMap('/path/to/schemas', [customSchema]);
173
+ * // schemasMap contains both YAML schemas and CustomSchema
174
+ * ```
175
+ *
176
+ * **Example - Overriding YAML Schemas:**
177
+ * ```typescript
178
+ * const updatedProfile = new Schema(
179
+ * { name: { type: 'string' }, newField: { type: 'number' } },
180
+ * 'Profile'
181
+ * );
182
+ * const schemasMap = createSchemasMap('/path/to/schemas', [updatedProfile]);
183
+ * // schemasMap.Profile is the updatedProfile instance, not the YAML version
184
+ * ```
185
+ *
186
+ * **Example - Filtering Non-Schema Values:**
187
+ * ```typescript
188
+ * const schema = new Schema({ field: { type: 'string' } }, 'Test');
189
+ * const schemasMap = createSchemasMap('/path/to/schemas', [
190
+ * schema,
191
+ * 'not a schema',
192
+ * { id: 'fake' },
193
+ * null
194
+ * ]);
195
+ * // Only the Schema instance is included, other values are ignored
196
+ * ```
197
+ */
198
+ const createSchemasMap = (servicePath: string, modules: unknown[]): Record<string, Schema> => {
199
+ const yamlSchemas = readSchemasSync(servicePath);
200
+ const schemasMap = keyBy(yamlSchemas, 'id');
201
+
202
+ const schemas = modules
203
+ .filter(schema => schema instanceof Schema);
204
+
205
+ for (const schema of schemas) {
206
+ schemasMap[schema.id] = schema;
207
+ }
208
+
209
+ return schemasMap;
210
+ };
211
+
212
+ export default createSchemasMap;
package/src/index.ts CHANGED
@@ -1,13 +1,17 @@
1
+ import got from './helpers/got';
1
2
  import Schema from './Schema';
2
3
  import Validator from './Validator';
3
4
  import documentLoader from './ld/documentLoader';
4
5
  import ValidationError from './ValidationError';
6
+ import createSchemasMap from './helpers/createSchemasMap';
5
7
  import CredentialFactory from './CredentialFactory';
6
8
 
7
9
  export {
10
+ got,
8
11
  Schema,
9
12
  Validator,
10
13
  documentLoader,
11
14
  ValidationError,
15
+ createSchemasMap,
12
16
  CredentialFactory
13
17
  };