@gefyra/complies-with-eu 1.2.0 → 1.3.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.
- package/CHANGELOG.md +21 -0
- package/README.md +19 -10
- package/bin/complies-with-eu.js +11 -3
- package/package.json +6 -6
- package/src/commands/search.js +8 -3
- package/src/commands/validate.js +82 -45
- package/src/lib/constraintCheck.js +19 -0
- package/src/lib/javaValidator.js +122 -47
- package/src/lib/javaValidatorServer.js +166 -23
- package/src/lib/output.js +44 -0
- package/src/lib/preflight.js +1 -1
- package/src/lib/reportGen.js +39 -5
- package/src/lib/toolDownloader.js +3 -3
- package/src/lib/validateCore.js +13 -6
- package/src/lib/validatorOutput.js +276 -0
package/src/lib/validateCore.js
CHANGED
|
@@ -12,13 +12,14 @@ const {
|
|
|
12
12
|
addMetaProfileClaims,
|
|
13
13
|
} = require('./profileUtils');
|
|
14
14
|
const { runValidatorBatch } = require('./javaValidator');
|
|
15
|
+
const { oneLineError } = require('./validatorOutput');
|
|
15
16
|
const { resolvePackageId, resolvePackageDependencyDirs } = require('./fhirPkgTool');
|
|
16
17
|
const { generateReport, mdCode, mdText, exampleErrorCount } = require('./reportGen');
|
|
17
18
|
const { checkObligations } = require('./obligationCheck');
|
|
18
19
|
const { evaluateExistsOrConstraints, loadEuSds } = require('./constraintCheck');
|
|
19
20
|
const { readInstalledVersionsSync } = require('./toolDownloader');
|
|
20
21
|
|
|
21
|
-
const stderrLog = (
|
|
22
|
+
const stderrLog = require('./output').makeProgressLogger();
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Parses "packageName[@version]" (or "packageName#version") into { packageName, version }.
|
|
@@ -184,10 +185,11 @@ async function postProcessProfile({
|
|
|
184
185
|
|
|
185
186
|
// Evaluate exists()-or constraints
|
|
186
187
|
let filteredNonCompliantMap;
|
|
187
|
-
let autoSatisfiedConstraints
|
|
188
|
+
let autoSatisfiedConstraints;
|
|
189
|
+
let euSdByCanonical = new Map();
|
|
188
190
|
try {
|
|
189
191
|
const rawNonCompliantMap = collectNonCompliant(parsed.issues);
|
|
190
|
-
|
|
192
|
+
euSdByCanonical = await loadEuSds(euPackageDirs, euCanonicals);
|
|
191
193
|
({ filteredMap: filteredNonCompliantMap, satisfied: autoSatisfiedConstraints } =
|
|
192
194
|
evaluateExistsOrConstraints({ inputSd: profile, euSdByCanonical, nonCompliantMap: rawNonCompliantMap }));
|
|
193
195
|
} catch {
|
|
@@ -209,6 +211,7 @@ async function postProcessProfile({
|
|
|
209
211
|
obligationViolationsByEuCanonical,
|
|
210
212
|
filteredNonCompliantMap,
|
|
211
213
|
autoSatisfiedConstraints,
|
|
214
|
+
euSdByCanonical,
|
|
212
215
|
});
|
|
213
216
|
|
|
214
217
|
// Derive compliance
|
|
@@ -237,8 +240,10 @@ async function postProcessProfile({
|
|
|
237
240
|
* @param {string} params.examplesDir - Directory for example artifacts
|
|
238
241
|
* @param {string[]} params.euCanonicals - EU canonical URLs
|
|
239
242
|
* @param {string[]} params.igIds - IG identifiers for the validator
|
|
240
|
-
* @param {boolean} params.debug - Debug flag
|
|
243
|
+
* @param {boolean} params.debug - Debug flag (implies verbose)
|
|
244
|
+
* @param {boolean} [params.verbose] - Stream the full validator output
|
|
241
245
|
* @param {Function} [params.log] - Progress logger (default: stderr)
|
|
246
|
+
* @param {Function} [params.runBatch] - runValidatorBatch replacement (tests)
|
|
242
247
|
* @returns {Promise<{exampleResults: Array, exReportPath: string}|null>}
|
|
243
248
|
*/
|
|
244
249
|
async function runExamplesForProfile({
|
|
@@ -249,7 +254,9 @@ async function runExamplesForProfile({
|
|
|
249
254
|
euCanonicals,
|
|
250
255
|
igIds,
|
|
251
256
|
debug = false,
|
|
257
|
+
verbose = false,
|
|
252
258
|
log = stderrLog,
|
|
259
|
+
runBatch = runValidatorBatch,
|
|
253
260
|
}) {
|
|
254
261
|
let examples;
|
|
255
262
|
try {
|
|
@@ -282,11 +289,11 @@ async function runExamplesForProfile({
|
|
|
282
289
|
let batchResults;
|
|
283
290
|
let batchError = null;
|
|
284
291
|
try {
|
|
285
|
-
({ results: batchResults } = await
|
|
292
|
+
({ results: batchResults } = await runBatch(examplesDir, igIds, { debug, verbose }, resolveArgs));
|
|
286
293
|
} catch (err) {
|
|
287
294
|
log(chalk.red(`Example validation failed: ${err.message}`));
|
|
288
295
|
batchResults = [];
|
|
289
|
-
batchError = err
|
|
296
|
+
batchError = oneLineError(err); // one line: repeated per example and inlined in the report
|
|
290
297
|
}
|
|
291
298
|
|
|
292
299
|
// Map results; a missing result is kept as summary:null and reported as failed
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handling of the HL7 FHIR Validator console output: by default the raw output
|
|
5
|
+
* (hundreds of "Information @ ..." lines) is NOT mirrored to the terminal. It is
|
|
6
|
+
* collected (and written to a log file by the callers) while a concise progress
|
|
7
|
+
* indicator on stderr shows coarse phase hints. With --verbose (or --debug) the
|
|
8
|
+
* raw output is streamed to stderr as before.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is free of Java/child-process dependencies so it can be unit-tested.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const ora = require('ora');
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line no-control-regex
|
|
17
|
+
const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g;
|
|
18
|
+
|
|
19
|
+
/** Number of trailing output lines included in error messages. */
|
|
20
|
+
const TAIL_LINES = 20;
|
|
21
|
+
|
|
22
|
+
/** Removes ANSI escape codes. */
|
|
23
|
+
function stripAnsi(text) {
|
|
24
|
+
return String(text).replace(ANSI_RE, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Coarse phases of a validator run. The validator connects to the terminology
|
|
29
|
+
* server after loading the core packages and only then loads the -ig packages
|
|
30
|
+
* (more "Load ..." / "Installing ..." lines), so until "Validating" is reached
|
|
31
|
+
* the label follows the most recent hint in either direction. "Validating" is
|
|
32
|
+
* final: later lines never move the progress back.
|
|
33
|
+
*/
|
|
34
|
+
const PHASES = [
|
|
35
|
+
{ label: 'Loading packages', re: /^\s*(?:Loading\b|Load\s|Installing\s|Package Summary)/ },
|
|
36
|
+
{ label: 'Connecting to terminology server', re: /^\s*Terminology server\b/ },
|
|
37
|
+
{ label: 'Validating', re: /^\s*(?:Validating\b|Validate\s|-- Validating\b|Get set)/, final: true },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns the index into PHASES that a single output line indicates, or -1.
|
|
42
|
+
* @param {string} line
|
|
43
|
+
* @returns {number}
|
|
44
|
+
*/
|
|
45
|
+
function detectPhase(line) {
|
|
46
|
+
const clean = stripAnsi(line);
|
|
47
|
+
for (let i = PHASES.length - 1; i >= 0; i--) {
|
|
48
|
+
if (PHASES[i].re.test(clean)) return i;
|
|
49
|
+
}
|
|
50
|
+
return -1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extracts the most useful closing information from the raw output: the last
|
|
55
|
+
* "*FAILURE*: ..." or "*SUCCESS*: ..." summary line and the "Done. Times: ..." line.
|
|
56
|
+
* @param {string} rawOutput
|
|
57
|
+
* @returns {{ summaryLine: string|null, doneLine: string|null, summaryCount: number }}
|
|
58
|
+
*/
|
|
59
|
+
function extractResultInfo(rawOutput) {
|
|
60
|
+
let summaryLine = null;
|
|
61
|
+
let doneLine = null;
|
|
62
|
+
let summaryCount = 0;
|
|
63
|
+
for (const line of stripAnsi(rawOutput).split(/\r\n|\r|\n/)) {
|
|
64
|
+
if (/^\*(FAILURE|SUCCESS)\*:/.test(line)) {
|
|
65
|
+
summaryLine = line.trim();
|
|
66
|
+
summaryCount++;
|
|
67
|
+
} else if (/^\s*Done\. Times:/.test(line)) {
|
|
68
|
+
doneLine = line.trim();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { summaryLine, doneLine, summaryCount };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns the last `n` non-empty lines of the output (ANSI codes removed).
|
|
76
|
+
* @param {string} rawOutput
|
|
77
|
+
* @param {number} [n]
|
|
78
|
+
* @returns {string[]}
|
|
79
|
+
*/
|
|
80
|
+
function tailLines(rawOutput, n = TAIL_LINES) {
|
|
81
|
+
const lines = stripAnsi(rawOutput || '')
|
|
82
|
+
.split(/\r\n|\r|\n/)
|
|
83
|
+
.map(l => l.replace(/\s+$/, ''))
|
|
84
|
+
.filter(l => l.trim() !== '');
|
|
85
|
+
return lines.slice(-n);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Builds an error message for an abnormal validator run that is understandable
|
|
90
|
+
* without --verbose: exit code, the last lines of output and the log file path.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} p
|
|
93
|
+
* @param {string} p.reason - What went wrong (e.g. "produced no result summary")
|
|
94
|
+
* @param {number|null} [p.code] - Exit code (null: unknown / still running)
|
|
95
|
+
* @param {string|null} [p.signal] - Terminating signal
|
|
96
|
+
* @param {string} [p.rawOutput]
|
|
97
|
+
* @param {string|null} [p.logPath]
|
|
98
|
+
* @param {string} [p.subject] - Who failed (default "FHIR Java Validator")
|
|
99
|
+
* @param {number} [p.tail]
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function formatValidatorError({ reason, code = null, signal = null, rawOutput = '', logPath = null, subject = 'FHIR Java Validator', tail = TAIL_LINES }) {
|
|
103
|
+
let exit = '';
|
|
104
|
+
if (signal) exit = `was terminated by signal ${signal} and `;
|
|
105
|
+
else if (code !== null && code !== undefined) exit = `exited with code ${code} and `;
|
|
106
|
+
const parts = [`${subject} ${exit}${reason}.`];
|
|
107
|
+
const lines = tailLines(rawOutput, tail);
|
|
108
|
+
if (lines.length > 0) {
|
|
109
|
+
parts.push(`Last ${lines.length} line(s) of validator output:\n${lines.map(l => ` ${l}`).join('\n')}`);
|
|
110
|
+
} else {
|
|
111
|
+
parts.push('The validator produced no output.');
|
|
112
|
+
}
|
|
113
|
+
if (logPath) parts.push(`Full validator output: ${logPath}`);
|
|
114
|
+
return parts.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One-line form of formatValidatorError (no output tail), for places where the
|
|
119
|
+
* message is repeated per file or embedded in a sentence: per-example errors in
|
|
120
|
+
* the --json output and in Markdown reports.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} p - Same fields as formatValidatorError (rawOutput/tail are ignored)
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
function formatValidatorErrorShort({ reason, code = null, signal = null, logPath = null, subject = 'FHIR Java Validator' }) {
|
|
126
|
+
let exit = '';
|
|
127
|
+
if (signal) exit = ` (terminated by signal ${signal})`;
|
|
128
|
+
else if (code !== null && code !== undefined) exit = ` (exit code ${code})`;
|
|
129
|
+
return `${subject} ${reason}${exit}.${logPath ? ` Full validator output: ${logPath}` : ''}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Creates an Error whose message is the full formatValidatorError text and
|
|
134
|
+
* whose `shortMessage` is the one-line form. `exitCode`, `signal` and `logPath`
|
|
135
|
+
* are attached as well.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} p - Fields of formatValidatorError
|
|
138
|
+
* @param {ErrorOptions} [errorOptions]
|
|
139
|
+
* @returns {Error & { shortMessage: string, exitCode: number|null, signal: string|null, logPath: string|null }}
|
|
140
|
+
*/
|
|
141
|
+
function createValidatorError(p, errorOptions) {
|
|
142
|
+
const err = new Error(formatValidatorError(p), errorOptions);
|
|
143
|
+
err.shortMessage = formatValidatorErrorShort(p);
|
|
144
|
+
err.exitCode = p.code ?? null;
|
|
145
|
+
err.signal = p.signal ?? null;
|
|
146
|
+
err.logPath = p.logPath ?? null;
|
|
147
|
+
return err;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Returns a one-line description of an error: its `shortMessage` if present,
|
|
152
|
+
* otherwise the first line of its message (plus the "Full validator output"
|
|
153
|
+
* line, if the message has one).
|
|
154
|
+
*
|
|
155
|
+
* @param {Error|string} err
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
function oneLineError(err) {
|
|
159
|
+
if (err && typeof err === 'object' && err.shortMessage) return err.shortMessage;
|
|
160
|
+
const msg = String(err && typeof err === 'object' ? err.message : err);
|
|
161
|
+
const lines = msg.split(/\r\n|\r|\n/);
|
|
162
|
+
if (lines.length === 1) return msg;
|
|
163
|
+
const logLine = lines.find(l => /^Full validator output: /.test(l));
|
|
164
|
+
return logLine ? `${lines[0]} ${logLine}` : lines[0];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Creates a handler for validator output chunks. It collects the full output,
|
|
169
|
+
* mirrors it to `write` only when `verbose` is set, and reports phase changes
|
|
170
|
+
* (detected line by line, robust against chunks that split lines).
|
|
171
|
+
*
|
|
172
|
+
* @param {object} [opts]
|
|
173
|
+
* @param {boolean} [opts.verbose] - Mirror every chunk to `write`
|
|
174
|
+
* @param {Function} [opts.write] - Sink for mirrored output (default: stderr, gray)
|
|
175
|
+
* @param {Function} [opts.onPhase] - Called with the phase label when the phase changes
|
|
176
|
+
* @param {Function} [opts.onChunk] - Called with every raw chunk (e.g. to append to a log file)
|
|
177
|
+
* @param {number} [opts.maxChars] - Keep only (roughly) the last maxChars characters in memory
|
|
178
|
+
* (for long-running processes; the log file has everything)
|
|
179
|
+
* @returns {{ onData: Function, flush: Function, getOutput: Function, getPhase: Function }}
|
|
180
|
+
*/
|
|
181
|
+
function createOutputHandler({ verbose = false, write, onPhase, onChunk, maxChars = 0 } = {}) {
|
|
182
|
+
const sink = write || ((text) => process.stderr.write(chalk.gray(text)));
|
|
183
|
+
const chunks = [];
|
|
184
|
+
let buffered = 0;
|
|
185
|
+
let partial = '';
|
|
186
|
+
let phaseIdx = -1;
|
|
187
|
+
|
|
188
|
+
const handleLine = (line) => {
|
|
189
|
+
const idx = detectPhase(line);
|
|
190
|
+
if (idx < 0 || idx === phaseIdx) return;
|
|
191
|
+
if (phaseIdx >= 0 && PHASES[phaseIdx].final) return;
|
|
192
|
+
phaseIdx = idx;
|
|
193
|
+
if (onPhase) onPhase(PHASES[idx].label);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const onData = (data) => {
|
|
197
|
+
const text = data.toString();
|
|
198
|
+
chunks.push(text);
|
|
199
|
+
buffered += text.length;
|
|
200
|
+
while (maxChars > 0 && buffered > maxChars && chunks.length > 1) {
|
|
201
|
+
buffered -= chunks.shift().length;
|
|
202
|
+
}
|
|
203
|
+
if (onChunk) onChunk(text);
|
|
204
|
+
if (verbose) sink(text);
|
|
205
|
+
const lines = (partial + text).split(/\r\n|\r|\n/);
|
|
206
|
+
partial = lines.pop();
|
|
207
|
+
for (const line of lines) handleLine(line);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const flush = () => {
|
|
211
|
+
if (partial) handleLine(partial);
|
|
212
|
+
partial = '';
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
onData,
|
|
217
|
+
flush,
|
|
218
|
+
getOutput: () => chunks.join(''),
|
|
219
|
+
getPhase: () => (phaseIdx >= 0 ? PHASES[phaseIdx].label : null),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Concise progress indicator for a validator run on stderr.
|
|
225
|
+
*
|
|
226
|
+
* - Default, TTY: an ora spinner whose text shows the current phase.
|
|
227
|
+
* - Default, non-TTY (CI, piped stderr): ora prints no frames; phase changes are
|
|
228
|
+
* written as single lines instead, so the log stays short and readable.
|
|
229
|
+
* - Verbose: no spinner (it would garble the streamed output); start and end are
|
|
230
|
+
* printed as plain lines.
|
|
231
|
+
*
|
|
232
|
+
* @param {object} opts
|
|
233
|
+
* @param {string} opts.text - Base text, e.g. "Running HL7 FHIR Validator"
|
|
234
|
+
* @param {boolean} [opts.verbose]
|
|
235
|
+
* @param {NodeJS.WritableStream} [opts.stream] - default process.stderr
|
|
236
|
+
* @param {Function} [opts.createSpinner] - injectable ora factory (tests)
|
|
237
|
+
*/
|
|
238
|
+
function createProgress({ text, verbose = false, stream = process.stderr, createSpinner } = {}) {
|
|
239
|
+
const write = (s) => stream.write(s);
|
|
240
|
+
if (verbose) {
|
|
241
|
+
write(chalk.cyan(`${text}...\n`));
|
|
242
|
+
return {
|
|
243
|
+
phase() {},
|
|
244
|
+
succeed(msg) { write(chalk.green(`\n✔ ${msg}\n`)); },
|
|
245
|
+
fail(msg) { write(chalk.red(`\n✖ ${msg}\n`)); },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const spinner = (createSpinner || ((o) => ora(o)))({ text: `${text}...`, stream }).start();
|
|
250
|
+
let lastPhase = null;
|
|
251
|
+
return {
|
|
252
|
+
phase(label) {
|
|
253
|
+
if (!label || label === lastPhase) return;
|
|
254
|
+
lastPhase = label;
|
|
255
|
+
if (spinner.isEnabled) spinner.text = `${text}: ${label}...`;
|
|
256
|
+
else write(chalk.gray(` ${label}...\n`));
|
|
257
|
+
},
|
|
258
|
+
succeed(msg) { spinner.succeed(msg); },
|
|
259
|
+
fail(msg) { spinner.fail(msg); },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
TAIL_LINES,
|
|
265
|
+
PHASES,
|
|
266
|
+
stripAnsi,
|
|
267
|
+
detectPhase,
|
|
268
|
+
extractResultInfo,
|
|
269
|
+
tailLines,
|
|
270
|
+
formatValidatorError,
|
|
271
|
+
formatValidatorErrorShort,
|
|
272
|
+
createValidatorError,
|
|
273
|
+
oneLineError,
|
|
274
|
+
createOutputHandler,
|
|
275
|
+
createProgress,
|
|
276
|
+
};
|