@xml-auditfile/auditfile 0.0.0-stage → 0.0.2-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.
- package/LICENSE +201 -0
- package/README.md +183 -2
- package/package.json +62 -4
- package/src/auditfile.js +186 -0
- package/src/definitions/dialect.js +40 -0
- package/src/definitions/element-definition.js +27 -0
- package/src/definitions/localized-text.js +41 -0
- package/src/definitions/manifest.js +160 -0
- package/src/definitions/names.js +42 -0
- package/src/definitions/resources.js +29 -0
- package/src/definitions/view-definition.js +160 -0
- package/src/diagnostics/collector.js +139 -0
- package/src/diagnostics/diagnostic.js +53 -0
- package/src/diagnostics/report.js +56 -0
- package/src/generated/spec.js +74 -0
- package/src/index.js +20 -0
- package/src/internal/composite-key.js +2 -0
- package/src/internal/decimal.js +193 -0
- package/src/internal/encoding.js +118 -0
- package/src/internal/json.js +106 -0
- package/src/internal/sha256.js +147 -0
- package/src/internal/streams.js +232 -0
- package/src/internal/xml-reader.js +247 -0
- package/src/memory/memory-sink.js +68 -0
- package/src/reading/mapping-plan.js +91 -0
- package/src/reading/read-options.js +136 -0
- package/src/reading/read-session.js +446 -0
- package/src/reading/row-loader.js +362 -0
- package/src/reading/row.js +123 -0
- package/src/reading/source.js +146 -0
- package/src/reading/value-converter.js +136 -0
- package/src/schema/schema.js +256 -0
- package/src/sinks/sink.js +53 -0
- package/src/validation/reference-checker.js +146 -0
- package/src/validation/xaf-rules.js +167 -0
- package/src/versioning/version-resolver.js +119 -0
- package/src/versioning/version.js +217 -0
- package/src/views/view-result.js +20 -0
- package/src/views/view-runner.js +449 -0
- package/src/xsd/datatypes.js +141 -0
- package/src/xsd/regex.js +121 -0
- package/src/xsd/schema-set.js +555 -0
- package/src/xsd/validator.js +484 -0
- package/types/auditfile.d.ts +57 -0
- package/types/definitions/dialect.d.ts +27 -0
- package/types/definitions/element-definition.d.ts +16 -0
- package/types/definitions/localized-text.d.ts +25 -0
- package/types/definitions/manifest.d.ts +118 -0
- package/types/definitions/names.d.ts +25 -0
- package/types/definitions/resources.d.ts +9 -0
- package/types/definitions/view-definition.d.ts +85 -0
- package/types/diagnostics/collector.d.ts +38 -0
- package/types/diagnostics/diagnostic.d.ts +37 -0
- package/types/diagnostics/report.d.ts +32 -0
- package/types/generated/spec.d.ts +6 -0
- package/types/index.d.ts +18 -0
- package/types/internal/composite-key.d.ts +2 -0
- package/types/internal/decimal.d.ts +55 -0
- package/types/internal/encoding.d.ts +43 -0
- package/types/internal/json.d.ts +17 -0
- package/types/internal/sha256.d.ts +17 -0
- package/types/internal/streams.d.ts +35 -0
- package/types/internal/xml-reader.d.ts +50 -0
- package/types/memory/memory-sink.d.ts +29 -0
- package/types/reading/mapping-plan.d.ts +45 -0
- package/types/reading/read-options.d.ts +69 -0
- package/types/reading/read-session.d.ts +28 -0
- package/types/reading/row-loader.d.ts +18 -0
- package/types/reading/row.d.ts +53 -0
- package/types/reading/source.d.ts +73 -0
- package/types/reading/value-converter.d.ts +15 -0
- package/types/schema/schema.d.ts +116 -0
- package/types/sinks/sink.d.ts +59 -0
- package/types/validation/reference-checker.d.ts +11 -0
- package/types/validation/xaf-rules.d.ts +13 -0
- package/types/versioning/version-resolver.d.ts +25 -0
- package/types/versioning/version.d.ts +71 -0
- package/types/views/view-result.d.ts +13 -0
- package/types/views/view-runner.d.ts +1 -0
- package/types/xsd/datatypes.d.ts +40 -0
- package/types/xsd/regex.d.ts +6 -0
- package/types/xsd/schema-set.d.ts +20 -0
- package/types/xsd/validator.d.ts +48 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { SEPARATOR } from '../internal/composite-key.js';
|
|
2
|
+
import { Decimal } from '../internal/decimal.js';
|
|
3
|
+
import { digitsOf, normalizeWhiteSpace } from './datatypes.js';
|
|
4
|
+
import { qualified } from './schema-set.js';
|
|
5
|
+
|
|
6
|
+
const XSI = 'http://www.w3.org/2001/XMLSchema-instance';
|
|
7
|
+
const XMLNS = 'http://www.w3.org/2000/xmlns/';
|
|
8
|
+
const MAX_PENDING_REFERENCES = 100_000;
|
|
9
|
+
const MAX_SHOWN = 100;
|
|
10
|
+
const NONE = Object.freeze([]);
|
|
11
|
+
|
|
12
|
+
/** Shortens a value for a message; values come from the file and can be long. */
|
|
13
|
+
function shown(value) {
|
|
14
|
+
return value.length > MAX_SHOWN ? `${value.slice(0, MAX_SHOWN)}…` : value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function lengthOf(value) {
|
|
18
|
+
let length = 0;
|
|
19
|
+
for (const _ of value) {
|
|
20
|
+
length++;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validates a simple value against a compiled simple type.
|
|
28
|
+
* @returns {{ value: string } | { error: string }}
|
|
29
|
+
*/
|
|
30
|
+
export function validateSimple(type, raw) {
|
|
31
|
+
const text = normalizeWhiteSpace(raw, type.whiteSpace);
|
|
32
|
+
const parsed = type.builtin.parse(text);
|
|
33
|
+
if (parsed === null) {
|
|
34
|
+
return { error: `'${shown(text)}' is not a valid value of the atomic type '${type.name}'.` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const steps = [];
|
|
38
|
+
for (let step = type; step !== null; step = step.base) {
|
|
39
|
+
if (step.facets !== null) {
|
|
40
|
+
steps.unshift(step);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const { facets } of steps) {
|
|
45
|
+
const error = checkFacets(facets, text, parsed);
|
|
46
|
+
if (error !== null) {
|
|
47
|
+
return { error };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { value: text };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkFacets(facets, text, parsed) {
|
|
55
|
+
const numeric = parsed instanceof Decimal;
|
|
56
|
+
if (facets.enumeration !== null
|
|
57
|
+
&& !facets.enumeration.some((option) => (numeric && option instanceof Decimal ? option.equals(parsed) : option === text))) {
|
|
58
|
+
return `[facet 'enumeration'] The value '${shown(text)}' is not an element of the set of allowed values.`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (facets.patterns.length > 0 && !facets.patterns.some((p) => p.regex.test(text))) {
|
|
62
|
+
return `[facet 'pattern'] The value '${shown(text)}' is not accepted by the pattern '${facets.patterns.map((p) => p.source).join("' | '")}'.`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!numeric && (facets.length !== null || facets.minLength !== null || facets.maxLength !== null)) {
|
|
66
|
+
const length = lengthOf(text);
|
|
67
|
+
if (facets.length !== null && length !== facets.length) {
|
|
68
|
+
return `[facet 'length'] The value '${shown(text)}' has a length of '${length}'; this differs from the allowed length of '${facets.length}'.`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (facets.minLength !== null && length < facets.minLength) {
|
|
72
|
+
return `[facet 'minLength'] The value '${shown(text)}' has a length of '${length}'; this underruns the allowed minimum length of '${facets.minLength}'.`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (facets.maxLength !== null && length > facets.maxLength) {
|
|
76
|
+
return `[facet 'maxLength'] The value '${shown(text)}' has a length of '${length}'; this exceeds the allowed maximum length of '${facets.maxLength}'.`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (numeric) {
|
|
81
|
+
const digits = digitsOf(parsed);
|
|
82
|
+
if (facets.totalDigits !== null && digits.total > facets.totalDigits) {
|
|
83
|
+
return `[facet 'totalDigits'] The value '${shown(text)}' has more digits than are allowed ('${facets.totalDigits}').`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (facets.fractionDigits !== null && digits.fraction > facets.fractionDigits) {
|
|
87
|
+
return `[facet 'fractionDigits'] The value '${shown(text)}' has more fractional digits than are allowed ('${facets.fractionDigits}').`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (facets.minInclusive !== null && parsed.compareTo(facets.minInclusive) < 0) {
|
|
91
|
+
return `[facet 'minInclusive'] The value '${shown(text)}' is less than the minimum value allowed ('${facets.minInclusive}').`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (facets.maxInclusive !== null && parsed.compareTo(facets.maxInclusive) > 0) {
|
|
95
|
+
return `[facet 'maxInclusive'] The value '${shown(text)}' is greater than the maximum value allowed ('${facets.maxInclusive}').`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (facets.minExclusive !== null && parsed.compareTo(facets.minExclusive) <= 0) {
|
|
99
|
+
return `[facet 'minExclusive'] The value '${shown(text)}' must be greater than '${facets.minExclusive}'.`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (facets.maxExclusive !== null && parsed.compareTo(facets.maxExclusive) >= 0) {
|
|
103
|
+
return `[facet 'maxExclusive'] The value '${shown(text)}' must be less than '${facets.maxExclusive}'.`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The value used to compare key fields: numbers by value, everything else as normalized text. */
|
|
111
|
+
function keyValue(type, value) {
|
|
112
|
+
if (type?.builtin?.numeric) {
|
|
113
|
+
const parsed = type.builtin.parse(value);
|
|
114
|
+
if (parsed instanceof Decimal) {
|
|
115
|
+
return parsed.normalize().toString();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Streaming validator for a compiled {@link SchemaSet}. Feed it the element events of a document in order;
|
|
124
|
+
* it reports every violation through `report(message, line, column)` and never stops on its own.
|
|
125
|
+
*/
|
|
126
|
+
export class XsdValidator {
|
|
127
|
+
#set;
|
|
128
|
+
#report;
|
|
129
|
+
/** @type {object[]} */
|
|
130
|
+
#stack = [];
|
|
131
|
+
#ids = new Set();
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {import('./schema-set.js').SchemaSet} set
|
|
135
|
+
* @param {(message: string, line: number | null, column: number | null) => void} report
|
|
136
|
+
*/
|
|
137
|
+
#maxValueLength;
|
|
138
|
+
#identityConstraints;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {number} [maxValueLength] Longer values are not kept and not checked; the reader reports them (XAF-S008).
|
|
142
|
+
* @param {boolean} [identityConstraints] Evaluate key, unique and keyref; off when another check does that work.
|
|
143
|
+
*/
|
|
144
|
+
constructor(set, report, maxValueLength = Infinity, identityConstraints = true) {
|
|
145
|
+
this.#maxValueLength = maxValueLength;
|
|
146
|
+
this.#identityConstraints = identityConstraints;
|
|
147
|
+
this.#set = set;
|
|
148
|
+
this.#report = report;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @param {{ ns: string, local: string, attributes: { ns: string, local: string, value: string }[], line: number, column: number, depth: number }} event */
|
|
152
|
+
start(event) {
|
|
153
|
+
const parent = this.#stack.at(-1);
|
|
154
|
+
let declaration = null;
|
|
155
|
+
if (parent === undefined) {
|
|
156
|
+
declaration = this.#set.elements.get(qualified(event.ns, event.local)) ?? null;
|
|
157
|
+
if (declaration === null) {
|
|
158
|
+
this.#error(event, `Element '${event.local}': No matching global declaration available for the validation root.`);
|
|
159
|
+
}
|
|
160
|
+
} else if (!parent.skip) {
|
|
161
|
+
declaration = this.#child(parent, event);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const frame = declaration === null
|
|
165
|
+
? { skip: true, depth: event.depth, scopes: NONE, notified: NONE, active: NONE }
|
|
166
|
+
: this.#open(declaration, event);
|
|
167
|
+
// Only constraints that can still select or capture something below the parent see this element.
|
|
168
|
+
if (parent !== undefined && parent.active.length > 0) {
|
|
169
|
+
frame.notified = parent.active;
|
|
170
|
+
frame.active = parent.active.filter((scope) => scope.start(event));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (declaration !== null && declaration.constraints.length > 0 && this.#identityConstraints) {
|
|
174
|
+
frame.scopes = declaration.constraints.map((constraint) => new IdentityScope(constraint, event.depth));
|
|
175
|
+
for (const scope of frame.scopes) {
|
|
176
|
+
if (scope.constraint.kind === 'keyref') {
|
|
177
|
+
scope.lookup = () => this.#findTable(scope.constraint.refer, frame);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
frame.active = [...frame.active, ...frame.scopes];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.#stack.push(frame);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @param {{ text: string, line: number, column: number }} event */
|
|
188
|
+
text(event) {
|
|
189
|
+
const frame = this.#stack.at(-1);
|
|
190
|
+
if (frame === undefined || frame.skip) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (frame.type.kind === 'simple') {
|
|
195
|
+
if (frame.text !== null && frame.text.length + event.text.length <= this.#maxValueLength) {
|
|
196
|
+
frame.text += event.text;
|
|
197
|
+
} else {
|
|
198
|
+
frame.text = null;
|
|
199
|
+
}
|
|
200
|
+
} else if (frame.type.kind === 'complex' && !frame.textReported && /[^ \t\r\n]/.test(event.text)) {
|
|
201
|
+
frame.textReported = true;
|
|
202
|
+
this.#error(event, `Element '${frame.local}': Character content other than whitespace is not allowed because the content type is 'element-only'.`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** @param {{ ns: string, local: string, line: number, column: number, depth: number }} event */
|
|
207
|
+
end(event) {
|
|
208
|
+
const frame = this.#stack.pop();
|
|
209
|
+
if (frame === undefined) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let value = null;
|
|
214
|
+
if (!frame.skip) {
|
|
215
|
+
if (frame.type.kind === 'simple') {
|
|
216
|
+
const result = frame.text === null ? { value: null } : validateSimple(frame.type, frame.text);
|
|
217
|
+
if ('error' in result) {
|
|
218
|
+
this.#error(event, `Element '${frame.local}': ${result.error}`);
|
|
219
|
+
} else if (result.value !== null) {
|
|
220
|
+
value = keyValue(frame.type, result.value);
|
|
221
|
+
if (frame.type.builtin.id) {
|
|
222
|
+
this.#id(event, result.value);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else if (frame.type.kind === 'complex' && !frame.broken) {
|
|
226
|
+
const missing = frame.type.particles.slice(frame.index).find((p, i) => (i === 0 ? frame.count : 0) < p.min);
|
|
227
|
+
if (missing !== undefined) {
|
|
228
|
+
this.#error(event, `Element '${frame.local}': Missing child element(s). Expected is ( ${missing.local} ).`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const scope of frame.notified) {
|
|
234
|
+
scope.end(event, value, this);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (frame.scopes.length > 0) {
|
|
238
|
+
for (const scope of frame.scopes) {
|
|
239
|
+
if (scope.constraint.kind === 'keyref') {
|
|
240
|
+
scope.checkReferences(this.#findTable(scope.constraint.refer, frame), this);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** @internal Reports an identity-constraint violation. */
|
|
247
|
+
error(line, column, message) {
|
|
248
|
+
this.#report(message, line, column);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#error(event, message) {
|
|
252
|
+
this.#report(message, event.line ?? null, event.column ?? null);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#open(declaration, event) {
|
|
256
|
+
const type = declaration.type;
|
|
257
|
+
const frame = {
|
|
258
|
+
skip: type.kind === 'any', local: event.local, type, index: 0, count: 0, broken: false, text: '', textReported: false,
|
|
259
|
+
scopes: NONE, notified: NONE, active: NONE, depth: event.depth,
|
|
260
|
+
};
|
|
261
|
+
if (type.kind !== 'any') {
|
|
262
|
+
this.#attributes(type, event);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return frame;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#child(parent, event) {
|
|
269
|
+
if (parent.type.kind === 'simple') {
|
|
270
|
+
if (!parent.broken) {
|
|
271
|
+
parent.broken = true;
|
|
272
|
+
this.#error(event, `Element '${parent.local}': Element content is not allowed, because the type definition is simple.`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const particles = parent.type.particles;
|
|
279
|
+
if (parent.broken) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
while (parent.index < particles.length) {
|
|
284
|
+
const particle = particles[parent.index];
|
|
285
|
+
if (particle.ns === event.ns && particle.local === event.local) {
|
|
286
|
+
if (parent.count < particle.max) {
|
|
287
|
+
parent.count++;
|
|
288
|
+
return particle;
|
|
289
|
+
}
|
|
290
|
+
} else if (parent.count < particle.min) {
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
parent.index++;
|
|
295
|
+
parent.count = 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
parent.broken = true;
|
|
299
|
+
const expected = particles.slice(parent.index).filter((p, i) => i === 0 || p.min > 0).slice(0, 1).map((p) => p.local);
|
|
300
|
+
this.#error(event, expected.length > 0
|
|
301
|
+
? `Element '${event.local}': This element is not expected. Expected is ( ${expected[0]} ).`
|
|
302
|
+
: `Element '${event.local}': This element is not expected.`);
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#attributes(type, event) {
|
|
307
|
+
const seen = new Set();
|
|
308
|
+
for (const attribute of event.attributes) {
|
|
309
|
+
if (attribute.ns === XMLNS || attribute.ns === XSI || (attribute.ns === '' && attribute.local === 'xmlns')) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const key = qualified(attribute.ns, attribute.local);
|
|
314
|
+
const declared = type.kind === 'complex' ? type.attributes.get(key) : undefined;
|
|
315
|
+
if (declared === undefined) {
|
|
316
|
+
this.#error(event, `Element '${event.local}', attribute '${attribute.local}': The attribute '${attribute.local}' is not allowed.`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
seen.add(key);
|
|
321
|
+
const result = validateSimple(declared.type, attribute.value);
|
|
322
|
+
if ('error' in result) {
|
|
323
|
+
this.#error(event, `Element '${event.local}', attribute '${attribute.local}': ${result.error}`);
|
|
324
|
+
} else if (declared.type.builtin.id) {
|
|
325
|
+
this.#id(event, result.value);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (type.kind === 'complex') {
|
|
330
|
+
for (const [key, declared] of type.attributes) {
|
|
331
|
+
if (declared.required && !seen.has(key)) {
|
|
332
|
+
this.#error(event, `Element '${event.local}': The attribute '${declared.local}' is required but missing.`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#id(event, value) {
|
|
339
|
+
if (this.#ids.has(value)) {
|
|
340
|
+
this.#error(event, `ID '${shown(value)}' is already defined.`);
|
|
341
|
+
} else {
|
|
342
|
+
this.#ids.add(value);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** The key table a keyref refers to: declared on the same element or on an ancestor. */
|
|
347
|
+
#findTable(refer, frame) {
|
|
348
|
+
for (const candidate of [frame, ...this.#stack.toReversed()]) {
|
|
349
|
+
const scope = candidate.scopes?.find((s) => s.constraint.name === refer && s.constraint.kind !== 'keyref');
|
|
350
|
+
if (scope !== undefined) {
|
|
351
|
+
return scope.table;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Tracks one key, unique or keyref below the element that declares it. */
|
|
360
|
+
class IdentityScope {
|
|
361
|
+
/** @type {Set<string>} Key values (key, unique). */
|
|
362
|
+
table = new Set();
|
|
363
|
+
/** @type {{ value: string, line: number | null, column: number | null }[]} Unresolved references (keyref). */
|
|
364
|
+
pending = [];
|
|
365
|
+
/** @type {(() => Set<string> | null) | null} The referenced key table (keyref). */
|
|
366
|
+
lookup = null;
|
|
367
|
+
/** Whether the element at each relative depth matches the selector so far, per alternative. */
|
|
368
|
+
#matches = [];
|
|
369
|
+
/** @type {{ depth: number, values: (string | undefined)[], fieldMatches: boolean[][], line: number, column: number }[]} */
|
|
370
|
+
#selected = [];
|
|
371
|
+
|
|
372
|
+
#maxSteps;
|
|
373
|
+
|
|
374
|
+
constructor(constraint, depth) {
|
|
375
|
+
this.constraint = constraint;
|
|
376
|
+
this.depth = depth;
|
|
377
|
+
this.#maxSteps = Math.max(...constraint.selector.map((path) => path.steps.length));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Sees the start of an element below the declaring element. Returns whether its descendants matter. */
|
|
381
|
+
start(event) {
|
|
382
|
+
const relative = event.depth - this.depth;
|
|
383
|
+
if (relative > this.#maxSteps && this.#selected.length === 0) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const name = qualified(event.ns, event.local);
|
|
388
|
+
const matches = this.constraint.selector.map((path, i) => {
|
|
389
|
+
const step = path.steps[relative - 1];
|
|
390
|
+
return relative <= path.steps.length && (relative === 1 || this.#matches[relative - 1]?.[i] === true)
|
|
391
|
+
&& (step === null || step === name);
|
|
392
|
+
});
|
|
393
|
+
this.#matches[relative] = matches;
|
|
394
|
+
if (matches.some((match, i) => match && relative === this.constraint.selector[i].steps.length)) {
|
|
395
|
+
this.#selected.push({ depth: event.depth, values: this.constraint.fields.map(() => undefined), fieldMatches: [], line: event.line, column: event.column });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (const node of this.#selected) {
|
|
399
|
+
const below = event.depth - node.depth;
|
|
400
|
+
node.fieldMatches[below] = this.constraint.fields.map((field, i) => {
|
|
401
|
+
if (below === 0) {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const step = field.steps[below - 1];
|
|
406
|
+
return below <= field.steps.length && node.fieldMatches[below - 1]?.[i] === true && (step === null || step === name);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
this.constraint.fields.forEach((field, i) => {
|
|
410
|
+
if (field.attribute !== null && below === field.steps.length && node.fieldMatches[below][i]) {
|
|
411
|
+
const attribute = event.attributes.find((a) => qualified(a.ns, a.local) === field.attribute);
|
|
412
|
+
if (attribute !== undefined) {
|
|
413
|
+
node.values[i] = attribute.value;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return this.#selected.length > 0
|
|
420
|
+
|| matches.some((match, i) => match && relative < this.constraint.selector[i].steps.length);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
end(event, value, validator) {
|
|
424
|
+
for (const node of this.#selected) {
|
|
425
|
+
const below = event.depth - node.depth;
|
|
426
|
+
this.constraint.fields.forEach((field, i) => {
|
|
427
|
+
if (field.attribute === null && below === field.steps.length && node.fieldMatches[below]?.[i] && value !== null) {
|
|
428
|
+
node.values[i] = value;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const node = this.#selected.at(-1);
|
|
434
|
+
if (node !== undefined && node.depth === event.depth) {
|
|
435
|
+
this.#selected.pop();
|
|
436
|
+
this.#complete(node, validator);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
#complete(node, validator) {
|
|
441
|
+
const { kind, displayName } = this.constraint;
|
|
442
|
+
if (node.values.some((v) => v === undefined)) {
|
|
443
|
+
if (kind === 'key') {
|
|
444
|
+
validator.error(node.line, node.column, `Element: Not all fields of key identity-constraint '${displayName}' evaluate to a node.`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const value = node.values.join(SEPARATOR);
|
|
451
|
+
if (kind === 'keyref') {
|
|
452
|
+
// Key tables only grow, so a value found now is found at the end too; only the rest waits.
|
|
453
|
+
if (this.lookup?.()?.has(value)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (this.pending.length < MAX_PENDING_REFERENCES) {
|
|
458
|
+
this.pending.push({ value, line: node.line, column: node.column });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (this.table.has(value)) {
|
|
465
|
+
validator.error(node.line, node.column, `Duplicate key-sequence ['${shown(node.values.join("', '"))}'] in ${kind} identity-constraint '${displayName}'.`);
|
|
466
|
+
} else {
|
|
467
|
+
this.table.add(value);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
checkReferences(table, validator) {
|
|
472
|
+
if (table === null) {
|
|
473
|
+
validator.error(null, null, `The keyref '${this.constraint.displayName}' refers to a key that is not in scope.`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
for (const reference of this.pending) {
|
|
478
|
+
if (!table.has(reference.value)) {
|
|
479
|
+
validator.error(reference.line, reference.column,
|
|
480
|
+
`No match found for key-sequence ['${shown(reference.value.split(SEPARATOR).join("', '"))}'] of keyref '${this.constraint.displayName}'.`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { AuditfileSource } from './reading/source.js';
|
|
2
|
+
/**
|
|
3
|
+
* Entry point. Read an audit file into a sink, validate it, scan it, or stream its rows yourself.
|
|
4
|
+
* @example
|
|
5
|
+
* const sink = new MemorySink();
|
|
6
|
+
* const report = await Auditfile.read(AuditfileSource.fromFactory(() => fs.createReadStream('admin.xaf'), 'admin.xaf'), sink);
|
|
7
|
+
*/
|
|
8
|
+
export declare class Auditfile {
|
|
9
|
+
/**
|
|
10
|
+
* Reads an audit file into a sink. Verified mode (default) validates against the XSD first and loads only a valid file.
|
|
11
|
+
* @param {AuditfileSource} source
|
|
12
|
+
* @param {import('./sinks/sink.js').Sink} sink
|
|
13
|
+
* @param {import('./reading/read-options.js').ReadOptions} [options]
|
|
14
|
+
* @returns {Promise<import('./diagnostics/report.js').AuditfileReport>}
|
|
15
|
+
*/
|
|
16
|
+
static read(source: AuditfileSource, sink: import('./sinks/sink.js').Sink, options?: import('./reading/read-options.js').ReadOptions): Promise<import('./diagnostics/report.js').AuditfileReport>;
|
|
17
|
+
/**
|
|
18
|
+
* Validates an audit file (structure, keys, business rules) without storing anything.
|
|
19
|
+
* In verified mode (the default) the XSD is checked first and the rest only for a valid file;
|
|
20
|
+
* unverified mode skips the XSD and checks everything else, which is faster but reports fewer deviations
|
|
21
|
+
* (e.g. XAF-S004 instead of XAF-D001 for an invalid value).
|
|
22
|
+
* @param {AuditfileSource} source
|
|
23
|
+
* @param {import('./reading/read-options.js').ReadOptions} [options]
|
|
24
|
+
*/
|
|
25
|
+
static validate(source: AuditfileSource, options?: import('./reading/read-options.js').ReadOptions): Promise<import("./diagnostics/report.js").AuditfileReport>;
|
|
26
|
+
/**
|
|
27
|
+
* The scan phase only: determines the version, validates against the XSD in verified mode,
|
|
28
|
+
* and counts rows per table. The report's schema contains the row counts.
|
|
29
|
+
* @param {AuditfileSource} source
|
|
30
|
+
* @param {import('./reading/read-options.js').ReadOptions} [options]
|
|
31
|
+
*/
|
|
32
|
+
static scan(source: AuditfileSource, options?: import('./reading/read-options.js').ReadOptions): Promise<import("./diagnostics/report.js").AuditfileReport>;
|
|
33
|
+
/**
|
|
34
|
+
* Opens an audit file for reading rows yourself, without a sink.
|
|
35
|
+
* @param {AuditfileSource} source
|
|
36
|
+
* @param {import('./reading/read-options.js').ReadOptions} [options]
|
|
37
|
+
*/
|
|
38
|
+
static open(source: AuditfileSource, options?: import('./reading/read-options.js').ReadOptions): Promise<AuditfileReader>;
|
|
39
|
+
}
|
|
40
|
+
/** Reads rows one by one. Check {@link AuditfileReader#schema} for null before reading: null means the file cannot be read. */
|
|
41
|
+
export declare class AuditfileReader {
|
|
42
|
+
#private;
|
|
43
|
+
/** @type {import('./schema/schema.js').AuditfileSchema | null} The import schema, or null when the file cannot be read (see {@link AuditfileReader#report}). */
|
|
44
|
+
get schema(): import('./schema/schema.js').AuditfileSchema | null;
|
|
45
|
+
/**
|
|
46
|
+
* Rows in document order; a row is returned when its element closes. The rows are read while you iterate,
|
|
47
|
+
* and can be iterated only once: collect them in an array when you need them twice.
|
|
48
|
+
* @returns {AsyncGenerator<import('./reading/row.js').AuditfileRow>}
|
|
49
|
+
* @throws {Error} The rows are iterated a second time.
|
|
50
|
+
*/
|
|
51
|
+
readRows(): AsyncGenerator<import('./reading/row.js').AuditfileRow>;
|
|
52
|
+
/**
|
|
53
|
+
* The report; complete after {@link AuditfileReader#readRows} has been iterated to the end. Until then every call gives
|
|
54
|
+
* the report so far; the complete report is made once and kept.
|
|
55
|
+
*/
|
|
56
|
+
get report(): any;
|
|
57
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { LocalizedText } from './localized-text.js';
|
|
2
|
+
/** Corrections for a package that deviates from the standard, from dialects.json. */
|
|
3
|
+
export declare class Dialect {
|
|
4
|
+
/** Dialect name. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** What the dialect is for. */
|
|
7
|
+
description: LocalizedText | null;
|
|
8
|
+
/** Namespaces that select this dialect. */
|
|
9
|
+
namespaces: readonly string[];
|
|
10
|
+
/** Value of header/softwareDesc that selects this dialect. */
|
|
11
|
+
softwareDescription: string | null;
|
|
12
|
+
/** Corrections to apply. */
|
|
13
|
+
corrections: readonly DialectCorrection[];
|
|
14
|
+
}
|
|
15
|
+
/** One correction of a dialect. */
|
|
16
|
+
export declare class DialectCorrection {
|
|
17
|
+
/** Diagnostic code reported when the correction is applied. */
|
|
18
|
+
code: string;
|
|
19
|
+
/** What the correction does, one of {@link DialectAction}. */
|
|
20
|
+
action: string;
|
|
21
|
+
/** XML paths the correction applies to. */
|
|
22
|
+
paths: readonly string[];
|
|
23
|
+
/** Original value (replaceNamespace). */
|
|
24
|
+
from: string | null;
|
|
25
|
+
/** Replacement value (replaceNamespace). */
|
|
26
|
+
to: string | null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { LocalizedText } from './localized-text.js';
|
|
2
|
+
/** Documentation of a table, column or flattened group, from definition.json. */
|
|
3
|
+
export declare class ElementDefinition {
|
|
4
|
+
/** "tr_line" (table), "tr_line.amnt" (column) or "tr_line.currency" (group). */
|
|
5
|
+
key: string;
|
|
6
|
+
/** "table", "column" or "group". */
|
|
7
|
+
kind: string;
|
|
8
|
+
/** Official name, usually English only. */
|
|
9
|
+
label: LocalizedText | null;
|
|
10
|
+
/** Meaning according to the official documentation. */
|
|
11
|
+
definition: LocalizedText | null;
|
|
12
|
+
/** Practical explanation. */
|
|
13
|
+
description: LocalizedText | null;
|
|
14
|
+
/** @type {Map<string, LocalizedText>} Meaning of code values, e.g. "D" = Debit. */
|
|
15
|
+
codes: Map<string, LocalizedText>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Language for definitions and diagnostic messages. */
|
|
2
|
+
export declare const TextLanguage: Readonly<{
|
|
3
|
+
/** Dutch: the official language of the Belastingdienst publications. */
|
|
4
|
+
Dutch: "nl";
|
|
5
|
+
/** English; falls back to Dutch where no translation exists. */
|
|
6
|
+
English: "en";
|
|
7
|
+
}>;
|
|
8
|
+
/** A text in Dutch and English. English falls back to Dutch when empty. */
|
|
9
|
+
export declare class LocalizedText {
|
|
10
|
+
/** Dutch text, or null. */
|
|
11
|
+
dutch: string | null;
|
|
12
|
+
/** English text, or null. */
|
|
13
|
+
english: string | null;
|
|
14
|
+
/**
|
|
15
|
+
* @param {string | null} dutch
|
|
16
|
+
* @param {string | null} english
|
|
17
|
+
*/
|
|
18
|
+
constructor(dutch: string | null, english: string | null);
|
|
19
|
+
/**
|
|
20
|
+
* The text in the requested language, falling back to the other language.
|
|
21
|
+
* @param {'nl' | 'en'} [language]
|
|
22
|
+
*/
|
|
23
|
+
get(language?: 'nl' | 'en'): string | null;
|
|
24
|
+
toString(): string;
|
|
25
|
+
}
|