@living-architecture/riviere-extract-config-published-language 0.7.0

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,258 @@
1
+ import Ajv from 'ajv';
2
+ import addFormats from 'ajv-formats';
3
+ import rawSchema from '../../extraction-config.schema.json' with { type: 'json' };
4
+ /**
5
+ * Required extraction fields by component type.
6
+ * These fields must have extraction rules defined (unless notUsed: true).
7
+ */
8
+ const REQUIRED_FIELDS = {
9
+ api: ['apiType'],
10
+ event: ['eventName'],
11
+ eventHandler: ['subscribedEvents'],
12
+ domainOp: ['operationName'],
13
+ ui: ['route'],
14
+ useCase: [],
15
+ };
16
+ const COMPONENT_TYPES = [
17
+ 'api',
18
+ 'useCase',
19
+ 'domainOp',
20
+ 'event',
21
+ 'eventHandler',
22
+ 'ui',
23
+ ];
24
+ const ajv = new Ajv({ allErrors: true });
25
+ addFormats(ajv);
26
+ const validate = ajv.compile(rawSchema);
27
+ /**
28
+ * Type guard checking if data is a valid DraftConfiguration.
29
+ * @param data - Data to validate.
30
+ * @returns True if data matches the schema.
31
+ */
32
+ function isValidExtractionConfig(data) {
33
+ return validate(data) === true;
34
+ }
35
+ /**
36
+ * Converts AJV errors to ValidationError format.
37
+ * @param errors - AJV validation errors.
38
+ * @returns Array of ValidationError objects.
39
+ */
40
+ function mapAjvErrors(errors) {
41
+ /* v8 ignore start -- AJV supplies both the errors array and each error message after validation fails. */
42
+ if (!errors) {
43
+ return [];
44
+ }
45
+ return errors.map((e) => ({
46
+ path: e.instancePath || '/',
47
+ message: e.message ?? 'unknown error',
48
+ }));
49
+ /* v8 ignore stop */
50
+ }
51
+ function isNotUsed(rule) {
52
+ return rule !== undefined && 'notUsed' in rule && rule.notUsed === true;
53
+ }
54
+ function hasDetectionRule(rule) {
55
+ return rule !== undefined && 'find' in rule && 'where' in rule;
56
+ }
57
+ function getExtractedFields(rule) {
58
+ if (!rule || !('extract' in rule) || !rule.extract) {
59
+ return [];
60
+ }
61
+ return Object.keys(rule.extract);
62
+ }
63
+ function validateModuleExtractionRules(module, moduleIndex) {
64
+ const errors = [];
65
+ for (const componentType of COMPONENT_TYPES) {
66
+ const rule = module[componentType];
67
+ if (isNotUsed(rule)) {
68
+ continue;
69
+ }
70
+ if (!hasDetectionRule(rule)) {
71
+ continue;
72
+ }
73
+ const requiredFields = REQUIRED_FIELDS[componentType];
74
+ if (requiredFields.length === 0) {
75
+ continue;
76
+ }
77
+ const extractedFields = getExtractedFields(rule);
78
+ const missingFields = requiredFields.filter((field) => !extractedFields.includes(field));
79
+ if (missingFields.length > 0) {
80
+ errors.push({
81
+ path: `/modules/${moduleIndex}/${componentType}`,
82
+ message: `Missing required extraction rules: ${missingFields.join(', ')}. ` +
83
+ `Add extraction rules to the 'extract' block or use 'notUsed: true' if not extracting ${componentType} components.`,
84
+ });
85
+ }
86
+ }
87
+ return errors;
88
+ }
89
+ function validateAllExtractionRules(config) {
90
+ return config.modules.flatMap((module, index) => {
91
+ if ('$ref' in module) {
92
+ return [];
93
+ }
94
+ return validateModuleExtractionRules(module, index);
95
+ });
96
+ }
97
+ function collectCustomTypeExtractedFields(config) {
98
+ const fieldsByType = new Map();
99
+ for (const module of config.modules) {
100
+ if ('$ref' in module || module.customTypes === undefined) {
101
+ continue;
102
+ }
103
+ for (const [typeName, rule] of Object.entries(module.customTypes)) {
104
+ const existing = fieldsByType.get(typeName) ?? new Set();
105
+ for (const key of Object.keys(rule.extract ?? {})) {
106
+ existing.add(key);
107
+ }
108
+ fieldsByType.set(typeName, existing);
109
+ }
110
+ }
111
+ return fieldsByType;
112
+ }
113
+ function validateEventPublishers(connections, customTypeFields) {
114
+ if (connections.eventPublishers === undefined) {
115
+ return [];
116
+ }
117
+ return connections.eventPublishers.flatMap((publisher, index) => {
118
+ const extractedFields = customTypeFields.get(publisher.fromType);
119
+ if (extractedFields === undefined) {
120
+ return [
121
+ {
122
+ path: `/connections/eventPublishers/${index}/fromType`,
123
+ message: `"${publisher.fromType}" is not defined as a customType in any module. ` +
124
+ `Add a customType named "${publisher.fromType}" to at least one module.`,
125
+ },
126
+ ];
127
+ }
128
+ if (!extractedFields.has(publisher.metadataKey)) {
129
+ return [
130
+ {
131
+ path: `/connections/eventPublishers/${index}/fromType`,
132
+ message: `customType "${publisher.fromType}" does not extract "${publisher.metadataKey}". ` +
133
+ `Add extract["${publisher.metadataKey}"] to that custom type.`,
134
+ },
135
+ ];
136
+ }
137
+ return [];
138
+ });
139
+ }
140
+ function validateHttpLinks(connections, customTypeFields) {
141
+ if (connections.httpLinks === undefined) {
142
+ return [];
143
+ }
144
+ return connections.httpLinks.flatMap((httpLink, index) => {
145
+ const extractedFields = customTypeFields.get(httpLink.fromCustomType);
146
+ if (extractedFields === undefined) {
147
+ return [
148
+ {
149
+ path: `/connections/httpLinks/${index}/fromCustomType`,
150
+ message: `"${httpLink.fromCustomType}" is not defined as a customType in any module. ` +
151
+ `Add a customType named "${httpLink.fromCustomType}" to at least one module.`,
152
+ },
153
+ ];
154
+ }
155
+ const errors = [];
156
+ if (!extractedFields.has(httpLink.matchDomainBy)) {
157
+ errors.push({
158
+ path: `/connections/httpLinks/${index}/matchDomainBy`,
159
+ message: `customType "${httpLink.fromCustomType}" does not extract "${httpLink.matchDomainBy}". ` +
160
+ `Add extract["${httpLink.matchDomainBy}"] to that custom type.`,
161
+ });
162
+ }
163
+ for (const field of httpLink.matchApiBy) {
164
+ if (!extractedFields.has(field)) {
165
+ errors.push({
166
+ path: `/connections/httpLinks/${index}/matchApiBy`,
167
+ message: `customType "${httpLink.fromCustomType}" does not extract "${field}". ` +
168
+ `Add extract["${field}"] to that custom type.`,
169
+ });
170
+ }
171
+ }
172
+ return errors;
173
+ });
174
+ }
175
+ function validateConnectionsConfig(config) {
176
+ if (config.connections === undefined) {
177
+ return [];
178
+ }
179
+ const customTypeFields = collectCustomTypeExtractedFields(config);
180
+ return [
181
+ ...validateEventPublishers(config.connections, customTypeFields),
182
+ ...validateHttpLinks(config.connections, customTypeFields),
183
+ ];
184
+ }
185
+ /**
186
+ * Validates data against the DraftConfiguration JSON Schema only.
187
+ * Does NOT check semantic rules like required extraction fields.
188
+ * Use validateExtractionConfig() for full validation.
189
+ * @param data - Data to validate.
190
+ * @returns Validation result with errors if invalid.
191
+ */
192
+ function validateExtractionConfigSchema(data) {
193
+ const schemaValid = validate(data) === true;
194
+ if (!schemaValid) {
195
+ return {
196
+ valid: false,
197
+ errors: mapAjvErrors(validate.errors),
198
+ };
199
+ }
200
+ return {
201
+ valid: true,
202
+ errors: [],
203
+ };
204
+ }
205
+ /**
206
+ * Validates data against the DraftConfiguration schema.
207
+ * Performs both JSON Schema validation and semantic validation
208
+ * for required extraction rules.
209
+ * @param data - Data to validate.
210
+ * @returns Validation result with errors if invalid.
211
+ */
212
+ function validateExtractionConfig(data) {
213
+ // Type guard validates schema AND narrows type in one step
214
+ if (!isValidExtractionConfig(data)) {
215
+ // Get detailed error messages from schema validation
216
+ return validateExtractionConfigSchema(data);
217
+ }
218
+ // data is now narrowed to DraftConfiguration
219
+ const semanticErrors = [...validateAllExtractionRules(data), ...validateConnectionsConfig(data)];
220
+ if (semanticErrors.length > 0) {
221
+ return {
222
+ valid: false,
223
+ errors: semanticErrors,
224
+ };
225
+ }
226
+ return {
227
+ valid: true,
228
+ errors: [],
229
+ };
230
+ }
231
+ /**
232
+ * Parses an extraction configuration against the structural schema.
233
+ *
234
+ * @riviere-role published-language-parser
235
+ * @param data - Configuration value to parse
236
+ * @returns The draft configuration or its validation errors
237
+ */
238
+ export function parseExtractionConfigSchema(data) {
239
+ const result = validateExtractionConfigSchema(data);
240
+ if (result.valid && isValidExtractionConfig(data)) {
241
+ return { success: true, configuration: data };
242
+ }
243
+ return { success: false, errors: result.errors };
244
+ }
245
+ /**
246
+ * Parses and semantically validates an extraction configuration.
247
+ *
248
+ * @riviere-role published-language-parser
249
+ * @param data - Configuration value to parse
250
+ * @returns The draft configuration or its validation errors
251
+ */
252
+ export function parseExtractionConfig(data) {
253
+ const result = validateExtractionConfig(data);
254
+ if (result.valid && isValidExtractionConfig(data)) {
255
+ return { success: true, configuration: data };
256
+ }
257
+ return { success: false, errors: result.errors };
258
+ }