fhirsmith 0.12.0 → 0.13.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 +305 -0
- package/README.md +1 -1
- package/config-template.json +13 -0
- package/library/languages.js +191 -34
- package/library/path-safety.js +51 -0
- package/library/request-token.js +56 -0
- package/package.json +2 -2
- package/packages/packages.js +17 -118
- package/publisher/publisher.js +60 -30
- package/publisher/validation.js +15 -3
- package/server.js +12 -3
- package/translations/Messages.properties +1 -0
- package/tx/README.md +21 -0
- package/tx/cs/cs-api.js +30 -1
- package/tx/cs/cs-cs.js +1 -1
- package/tx/cs/cs-icd11.js +805 -0
- package/tx/cs/cs-lang.js +331 -85
- package/tx/cs/cs-loinc.js +45 -4
- package/tx/cs/cs-mimetypes.js +492 -21
- package/tx/cs/cs-snomed.js +38 -4
- package/tx/cs/cs-ucum.js +33 -3
- package/tx/cs/icd11-expressions.js +243 -0
- package/tx/importers/export-icd11-codesystem.js +823 -0
- package/tx/importers/icd11-schema.md +285 -0
- package/tx/importers/import-icd11.js +861 -0
- package/tx/importers/import-loinc.module.js +14 -7
- package/tx/importers/import-sct.module.js +10 -1
- package/tx/library/codesystem.js +21 -27
- package/tx/library/nesting-limit.js +79 -0
- package/tx/library/operation-outcome.js +97 -3
- package/tx/library/valueset.js +16 -20
- package/tx/library.js +23 -1
- package/tx/ocl/cache/cache-utils.cjs +5 -3
- package/tx/ocl/cs-ocl.cjs +73 -17
- package/tx/ocl/jobs/background-queue.cjs +6 -3
- package/tx/ocl/mappers/concept-mapper.cjs +9 -1
- package/tx/ocl/shared/patches.cjs +48 -1
- package/tx/ocl/vs-ocl.cjs +15 -13
- package/tx/sct/expressions.js +194 -17
- package/tx/tx.fhir.org.yml +2 -1
- package/tx/tx.js +15 -42
- package/tx/workers/batch-validate.js +6 -16
- package/tx/workers/batch.js +16 -28
- package/tx/workers/cache-control.js +6 -12
- package/tx/workers/closure.js +3 -8
- package/tx/workers/compare.js +6 -34
- package/tx/workers/expand.js +36 -45
- package/tx/workers/lookup.js +21 -39
- package/tx/workers/read.js +6 -40
- package/tx/workers/search.js +2 -8
- package/tx/workers/subsumes.js +58 -41
- package/tx/workers/translate.js +11 -17
- package/tx/workers/validate.js +76 -77
- package/tx/workers/worker.js +70 -1
- package/tx/xml/xml-base.js +243 -76
- package/tx/xversion/xv-valueset.js +39 -1
package/tx/cs/cs-lang.js
CHANGED
|
@@ -2,6 +2,7 @@ const { CodeSystemProvider, FilterExecutionContext, CodeSystemFactoryProvider} =
|
|
|
2
2
|
const { Language } = require('../../library/languages');
|
|
3
3
|
const { CodeSystem } = require("../library/codesystem");
|
|
4
4
|
const assert = require('assert');
|
|
5
|
+
const { Issue } = require("../library/operation-outcome");
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Language component types for filtering
|
|
@@ -18,14 +19,157 @@ const LanguageComponent = {
|
|
|
18
19
|
|
|
19
20
|
const CODES_LanguageComponent = Object.values(LanguageComponent);
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The components an `=` filter is offered on. These are the three with a bounded list
|
|
24
|
+
* of registered values behind them, which is what makes an expansion possible at all.
|
|
25
|
+
*/
|
|
26
|
+
const EQUALITY_COMPONENTS = [LanguageComponent.LANG, LanguageComponent.SCRIPT, LanguageComponent.REGION];
|
|
27
|
+
|
|
21
28
|
/**
|
|
22
29
|
* Filter context for language component filters
|
|
23
30
|
*/
|
|
31
|
+
/**
|
|
32
|
+
* How this code system validates a tag. Deliberately stricter than the default parse -
|
|
33
|
+
* see locate().
|
|
34
|
+
*/
|
|
35
|
+
const LANG_VALIDATION_OPTIONS = Object.freeze({ caseInsensitive: true, checkCombinations: true });
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Which part of a tag a filter is about, and how a Language exposes it.
|
|
39
|
+
* @param {Language} lang
|
|
40
|
+
* @param {string} component
|
|
41
|
+
* @returns {string} the component's value, '' when the tag does not state it
|
|
42
|
+
*/
|
|
43
|
+
function componentValue(lang, component) {
|
|
44
|
+
switch (component) {
|
|
45
|
+
case LanguageComponent.LANG: return lang.language || '';
|
|
46
|
+
case LanguageComponent.EXTLANG: return lang.extLang.join('-');
|
|
47
|
+
case LanguageComponent.SCRIPT: return lang.script || '';
|
|
48
|
+
case LanguageComponent.REGION: return lang.region || '';
|
|
49
|
+
case LanguageComponent.VARIANT: return lang.variant || '';
|
|
50
|
+
case LanguageComponent.EXTENSION: return lang.extension || '';
|
|
51
|
+
case LanguageComponent.PRIVATE_USE: return lang.privateUse.join('-');
|
|
52
|
+
default: return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `<component> exists true|false` - does the tag state this part at all?
|
|
58
|
+
*/
|
|
24
59
|
class IETFLanguageCodeFilter {
|
|
25
60
|
constructor(component, status) {
|
|
26
61
|
this.component = component; // LanguageComponent
|
|
27
62
|
this.status = status; // boolean - true if component must exist, false if must not exist
|
|
28
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {Language} lang
|
|
67
|
+
* @returns {boolean|string} true/false, or a message when the component is unknown
|
|
68
|
+
*/
|
|
69
|
+
matches(lang) {
|
|
70
|
+
const value = componentValue(lang, this.component);
|
|
71
|
+
if (value === null) {
|
|
72
|
+
return `Unknown language component: ${this.component}`;
|
|
73
|
+
}
|
|
74
|
+
return (value !== '') === this.status;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Why a code did not match, for the caller. @returns {string} */
|
|
78
|
+
describe(code, _lang) {
|
|
79
|
+
const action = this.status ? 'does not contain' : 'contains';
|
|
80
|
+
const requirement = this.status ? 'required' : 'not allowed';
|
|
81
|
+
return `The language code ${code} ${action} a ${this.component}, and it is ${requirement}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* `<component> = <value>` - the tag states this part, with this value.
|
|
87
|
+
*
|
|
88
|
+
* Comparison is case-insensitive, like everything else about BCP 47 tags.
|
|
89
|
+
*/
|
|
90
|
+
class IETFLanguageEqualityFilter {
|
|
91
|
+
constructor(component, value) {
|
|
92
|
+
this.component = component;
|
|
93
|
+
this.value = value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
matches(lang) {
|
|
97
|
+
const value = componentValue(lang, this.component);
|
|
98
|
+
if (value === null) {
|
|
99
|
+
return `Unknown language component: ${this.component}`;
|
|
100
|
+
}
|
|
101
|
+
return value.toLowerCase() === this.value.toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe(code, _lang) {
|
|
105
|
+
return `The language code ${code} does not have a ${this.component} of '${this.value}'`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The enumerable combinations.
|
|
111
|
+
*
|
|
112
|
+
* The BCP 47 grammar is unbounded, so a filter can only be expanded where fixing part
|
|
113
|
+
* of a tag leaves a finite list of registry entries to vary. Three combinations do:
|
|
114
|
+
*
|
|
115
|
+
* - language fixed: every region, plus the bare language, since a tag need state no
|
|
116
|
+
* region at all
|
|
117
|
+
* - region fixed: every language with that region
|
|
118
|
+
* - language AND region fixed: every script, plus the tag with no script
|
|
119
|
+
*
|
|
120
|
+
* Nothing else is enumerable - a script on its own leaves the language open, and there
|
|
121
|
+
* are as many tags as there are languages times everything else. Every one of these is
|
|
122
|
+
* still an incomplete answer, because variants, extensions and private-use subtags can
|
|
123
|
+
* always be added, so the expansion is marked unclosed.
|
|
124
|
+
*/
|
|
125
|
+
class IETFLanguageEnumerationFilter {
|
|
126
|
+
/**
|
|
127
|
+
* @param {string} mode - 'regions' | 'languages' | 'scripts'
|
|
128
|
+
* @param {object} fixed - { language, region } as far as they are fixed
|
|
129
|
+
* @param {LanguageDefinitions} definitions
|
|
130
|
+
* @param {IETFLanguageEqualityFilter[]} sources - the filters this replaces
|
|
131
|
+
*/
|
|
132
|
+
constructor(mode, fixed, definitions, sources) {
|
|
133
|
+
this.mode = mode;
|
|
134
|
+
this.fixed = fixed;
|
|
135
|
+
this.definitions = definitions;
|
|
136
|
+
this.sources = sources;
|
|
137
|
+
this.cursor = 0;
|
|
138
|
+
this._codes = null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The tags this filter covers, built once on demand.
|
|
143
|
+
* @returns {string[]}
|
|
144
|
+
*/
|
|
145
|
+
codes() {
|
|
146
|
+
if (this._codes === null) {
|
|
147
|
+
const d = this.definitions;
|
|
148
|
+
if (this.mode === 'regions') {
|
|
149
|
+
this._codes = [this.fixed.language]
|
|
150
|
+
.concat([...d.regions.keys()].map(r => `${this.fixed.language}-${r}`));
|
|
151
|
+
} else if (this.mode === 'languages') {
|
|
152
|
+
this._codes = [...d.languages.keys()].map(l => `${l}-${this.fixed.region}`);
|
|
153
|
+
} else {
|
|
154
|
+
this._codes = [`${this.fixed.language}-${this.fixed.region}`]
|
|
155
|
+
.concat([...d.scripts.keys()].map(sc => `${this.fixed.language}-${sc}-${this.fixed.region}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return this._codes;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
matches(lang) {
|
|
162
|
+
return this.sources.every(f => f.matches(lang) === true);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Only the rules that actually failed - saying `en-GB` has the wrong language, when
|
|
167
|
+
* it is the region that differs, sends the reader looking in the wrong place.
|
|
168
|
+
*/
|
|
169
|
+
describe(code, lang) {
|
|
170
|
+
const failed = lang ? this.sources.filter(f => f.matches(lang) !== true) : this.sources;
|
|
171
|
+
return (failed.length ? failed : this.sources).map(f => f.describe(code)).join('; ');
|
|
172
|
+
}
|
|
29
173
|
}
|
|
30
174
|
|
|
31
175
|
/**
|
|
@@ -86,13 +230,108 @@ class IETFLanguageCodeProvider extends CodeSystemProvider {
|
|
|
86
230
|
return super.hasAnyDisplays(langs);
|
|
87
231
|
}
|
|
88
232
|
|
|
233
|
+
// ========== Subsumption ==========
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Does one language tag subsume another?
|
|
237
|
+
*
|
|
238
|
+
* A tag is a set of named components - language, extended language, script, region,
|
|
239
|
+
* variant - not an ordered path, so one tag subsumes another when it says a subset of
|
|
240
|
+
* what the other says: `en` subsumes `en-US`, and `en-US` subsumes `en-Latn-US`,
|
|
241
|
+
* because everything true of the shorter tag is true of the longer one. The language
|
|
242
|
+
* itself must match; it is the one component that cannot be left unstated.
|
|
243
|
+
*
|
|
244
|
+
* This is RFC 4647's extended filtering rather than basic filtering. Basic filtering
|
|
245
|
+
* is strictly positional, so it would not have `en-US` subsume `en-Latn-US` - the
|
|
246
|
+
* script sits between the two subtags the range names. Extended filtering is the
|
|
247
|
+
* reading that matches what subsumption means, and it is the one that makes the
|
|
248
|
+
* relation transitive: `en` subsumes `en-US` subsumes `en-Latn-US`.
|
|
249
|
+
*
|
|
250
|
+
* Grandfathered tags are atomic. `i-klingon` has no components to compare - it does
|
|
251
|
+
* not decompose into valid subtags at all - so it relates only to itself.
|
|
252
|
+
*
|
|
253
|
+
* Note that Suppress-Script is deliberately not consulted: `en` carries
|
|
254
|
+
* `Suppress-Script: Latn`, which arguably makes `en-Latn` mean exactly `en`, but the
|
|
255
|
+
* registry calls that redundant rather than invalid and this server takes no position
|
|
256
|
+
* on it. So `en` subsumes `en-Latn` rather than being equivalent to it.
|
|
257
|
+
*
|
|
258
|
+
* @param {string} codeA
|
|
259
|
+
* @param {string} codeB
|
|
260
|
+
* @returns {Promise<string>} equivalent, subsumes, subsumed-by or not-subsumed
|
|
261
|
+
*/
|
|
262
|
+
async subsumesTest(codeA, codeB) {
|
|
263
|
+
const a = await this.#ensureContext(codeA);
|
|
264
|
+
const b = await this.#ensureContext(codeB);
|
|
265
|
+
if (!(a instanceof Language) || !(b instanceof Language)) {
|
|
266
|
+
return 'not-subsumed';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// A grandfathered tag is matched whole, so the only tag it stands in any relation to
|
|
270
|
+
// is itself (spelling aside - tags are case-insensitive).
|
|
271
|
+
if (a.wholeTag || b.wholeTag) {
|
|
272
|
+
return a.toString().toLowerCase() === b.toString().toLowerCase() ? 'equivalent' : 'not-subsumed';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const aCoversB = this.#covers(a, b);
|
|
276
|
+
const bCoversA = this.#covers(b, a);
|
|
277
|
+
if (aCoversB && bCoversA) {
|
|
278
|
+
return 'equivalent';
|
|
279
|
+
}
|
|
280
|
+
if (aCoversB) {
|
|
281
|
+
return 'subsumes';
|
|
282
|
+
}
|
|
283
|
+
if (bCoversA) {
|
|
284
|
+
return 'subsumed-by';
|
|
285
|
+
}
|
|
286
|
+
return 'not-subsumed';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Is everything `general` states also stated, identically, by `specific`?
|
|
291
|
+
*
|
|
292
|
+
* Comparison is case-insensitive throughout: the parts have already been canonicalised
|
|
293
|
+
* by the parser, but a tag that arrived in the wrong case must still take part in
|
|
294
|
+
* subsumption on equal terms.
|
|
295
|
+
*
|
|
296
|
+
* @param {Language} general
|
|
297
|
+
* @param {Language} specific
|
|
298
|
+
* @returns {boolean}
|
|
299
|
+
* @private
|
|
300
|
+
*/
|
|
301
|
+
#covers(general, specific) {
|
|
302
|
+
const same = (x, y) => (x || '').toLowerCase() === (y || '').toLowerCase();
|
|
303
|
+
const unstatedOrSame = (x, y) => !x || same(x, y);
|
|
304
|
+
|
|
305
|
+
if (!same(general.language, specific.language)) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
// Extended languages are ordered and there are at most three, so every one the
|
|
309
|
+
// general tag names has to appear in the specific tag, in the same order.
|
|
310
|
+
for (let i = 0; i < general.extLang.length; i++) {
|
|
311
|
+
if (!same(general.extLang[i], specific.extLang[i])) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return unstatedOrSame(general.script, specific.script)
|
|
316
|
+
&& unstatedOrSame(general.region, specific.region)
|
|
317
|
+
&& unstatedOrSame(general.variant, specific.variant)
|
|
318
|
+
&& unstatedOrSame(general.extension, specific.extension)
|
|
319
|
+
&& general.privateUse.every((p, i) => same(p, specific.privateUse[i]));
|
|
320
|
+
}
|
|
321
|
+
|
|
89
322
|
// ========== Code Information Methods ==========
|
|
90
323
|
|
|
91
324
|
async code(code) {
|
|
92
325
|
|
|
93
326
|
const ctxt = await this.#ensureContext(code);
|
|
94
327
|
if (ctxt instanceof Language) {
|
|
95
|
-
|
|
328
|
+
// The CANONICAL casing, not what the caller wrote. BCP 47 s2.1.1 makes tags
|
|
329
|
+
// case-insensitive but recommends a casing - lower case language, Titlecase
|
|
330
|
+
// script, UPPER CASE region - and toString() reassembles the tag from parts that
|
|
331
|
+
// were canonicalised as they were parsed. Returning it here is what lets the
|
|
332
|
+
// validator notice a difference and hand back a normalized-code, the same way it
|
|
333
|
+
// does for any other case-insensitive code system.
|
|
334
|
+
return ctxt.toString();
|
|
96
335
|
}
|
|
97
336
|
throw new Error('Invalid context type');
|
|
98
337
|
}
|
|
@@ -236,9 +475,19 @@ class IETFLanguageCodeProvider extends CodeSystemProvider {
|
|
|
236
475
|
assert(!code || typeof code === 'string', 'code must be string');
|
|
237
476
|
if (!code) return { context: null, message: 'Empty code' };
|
|
238
477
|
|
|
239
|
-
|
|
478
|
+
// Validating a code as a member of urn:ietf:bcp:47 is a stricter job than working
|
|
479
|
+
// out what a client asked for in an Accept-Language header, so this asks for the
|
|
480
|
+
// full check. The registry's own statements about subtag combinations - the Prefix
|
|
481
|
+
// on an extlang or variant - are applied, and a variant has to be registered at all.
|
|
482
|
+
// Case folding is on because BCP 47 s2.1.1 says tags are case-insensitive: 'en-us'
|
|
483
|
+
// is a valid way to write 'en-US', whatever the conventional casing recommends.
|
|
484
|
+
//
|
|
485
|
+
// These are OPTIONS rather than the default so that Accept-Language parsing keeps
|
|
486
|
+
// the behaviour it was written against - see Language's constructor.
|
|
487
|
+
const msg = {};
|
|
488
|
+
const language = this.languageDefinitions.parse(code, msg, LANG_VALIDATION_OPTIONS);
|
|
240
489
|
if (!language) {
|
|
241
|
-
return { context: null, message:
|
|
490
|
+
return { context: null, message: msg.message };
|
|
242
491
|
}
|
|
243
492
|
|
|
244
493
|
return { context: language, message: null };
|
|
@@ -256,6 +505,9 @@ class IETFLanguageCodeProvider extends CodeSystemProvider {
|
|
|
256
505
|
if (op === 'exists' && (value === 'true' || value === 'false')) {
|
|
257
506
|
return CODES_LanguageComponent.includes(prop);
|
|
258
507
|
}
|
|
508
|
+
if (op === '=') {
|
|
509
|
+
return EQUALITY_COMPONENTS.includes(prop);
|
|
510
|
+
}
|
|
259
511
|
return false;
|
|
260
512
|
}
|
|
261
513
|
|
|
@@ -266,6 +518,22 @@ class IETFLanguageCodeProvider extends CodeSystemProvider {
|
|
|
266
518
|
assert(op != null && typeof op === 'string', 'op must be a non-null string');
|
|
267
519
|
assert(value != null && typeof value === 'string', 'value must be a non-null string');
|
|
268
520
|
|
|
521
|
+
if (op === '=') {
|
|
522
|
+
if (!EQUALITY_COMPONENTS.includes(prop)) {
|
|
523
|
+
throw new Error(`The '=' filter is only supported on ${EQUALITY_COMPONENTS.join(', ')}, not '${prop}'`);
|
|
524
|
+
}
|
|
525
|
+
// The value has to be a registered subtag of the right kind, or the filter selects
|
|
526
|
+
// nothing and says nothing about why
|
|
527
|
+
const known = { language: this.languageDefinitions.languages,
|
|
528
|
+
script: this.languageDefinitions.scripts, region: this.languageDefinitions.regions }[prop];
|
|
529
|
+
const canonical = [...known.keys()].find(k => k.toLowerCase() === value.toLowerCase());
|
|
530
|
+
if (!canonical) {
|
|
531
|
+
throw new Error(`'${value}' is not a valid ${prop} subtag, so it cannot be filtered on`);
|
|
532
|
+
}
|
|
533
|
+
filterContext.filters.push(new IETFLanguageEqualityFilter(prop, canonical));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
269
537
|
if (op !== 'exists') {
|
|
270
538
|
throw new Error(`Unsupported filter operator: ${op}`);
|
|
271
539
|
}
|
|
@@ -288,15 +556,46 @@ class IETFLanguageCodeProvider extends CodeSystemProvider {
|
|
|
288
556
|
async executeFilters(filterContext) {
|
|
289
557
|
|
|
290
558
|
assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext');
|
|
291
|
-
|
|
559
|
+
|
|
560
|
+
// Whether anything can be enumerated depends on the filters TOGETHER, not one at a
|
|
561
|
+
// time: language alone gives regions, region alone gives languages, and the two
|
|
562
|
+
// together give scripts. So the equality filters are collapsed into a single set
|
|
563
|
+
// that knows which of those it is. Anything else is left alone and acts as a
|
|
564
|
+
// predicate - see filterCheck.
|
|
565
|
+
const equality = filterContext.filters.filter(f => f instanceof IETFLanguageEqualityFilter);
|
|
566
|
+
const language = equality.find(f => f.component === LanguageComponent.LANG);
|
|
567
|
+
const region = equality.find(f => f.component === LanguageComponent.REGION);
|
|
568
|
+
const script = equality.find(f => f.component === LanguageComponent.SCRIPT);
|
|
569
|
+
|
|
570
|
+
let mode = null;
|
|
571
|
+
if (language && region && !script) {
|
|
572
|
+
mode = 'scripts';
|
|
573
|
+
} else if (language && !region && !script) {
|
|
574
|
+
mode = 'regions';
|
|
575
|
+
} else if (region && !language && !script) {
|
|
576
|
+
mode = 'languages';
|
|
577
|
+
}
|
|
578
|
+
if (!mode) {
|
|
579
|
+
return filterContext.filters;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const combined = new IETFLanguageEnumerationFilter(mode,
|
|
583
|
+
{ language: language ? language.value : null, region: region ? region.value : null },
|
|
584
|
+
this.languageDefinitions, [language, region].filter(f => f));
|
|
585
|
+
return [combined].concat(filterContext.filters.filter(f => f !== language && f !== region));
|
|
292
586
|
}
|
|
293
587
|
|
|
294
588
|
async filterSize(filterContext, set) {
|
|
295
589
|
|
|
296
590
|
assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext');
|
|
297
|
-
assert(set && set instanceof IETFLanguageCodeFilter, 'set must be a IETFLanguageCodeFilter');
|
|
298
591
|
|
|
299
|
-
|
|
592
|
+
if (set instanceof IETFLanguageEnumerationFilter) {
|
|
593
|
+
return set.codes().length;
|
|
594
|
+
}
|
|
595
|
+
throw new Issue('error', 'not-supported', null, 'CODESYSTEM_NOT_ENUMERABLE',
|
|
596
|
+
'This filter on urn:ietf:bcp:47 cannot be expanded: the language grammar is unbounded, and only '
|
|
597
|
+
+ 'a fixed language (giving its regions), a fixed region (giving its languages), or both '
|
|
598
|
+
+ '(giving the scripts) leave a finite list to enumerate', 'not-supported', 422);
|
|
300
599
|
}
|
|
301
600
|
|
|
302
601
|
async filtersNotClosed(filterContext) {
|
|
@@ -308,103 +607,56 @@ class IETFLanguageCodeProvider extends CodeSystemProvider {
|
|
|
308
607
|
async filterMore(filterContext, set) {
|
|
309
608
|
|
|
310
609
|
assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext');
|
|
311
|
-
|
|
312
|
-
|
|
610
|
+
|
|
611
|
+
if (set instanceof IETFLanguageEnumerationFilter) {
|
|
612
|
+
return set.cursor < set.codes().length;
|
|
613
|
+
}
|
|
614
|
+
// Throwing rather than answering false: a mistake here would otherwise surface as an
|
|
615
|
+
// empty expansion, which reads as "there are none" instead of "this cannot be done".
|
|
616
|
+
throw new Issue('error', 'not-supported', null, 'CODESYSTEM_NOT_ENUMERABLE',
|
|
617
|
+
'This filter on urn:ietf:bcp:47 cannot be expanded', 'not-supported', 422);
|
|
313
618
|
}
|
|
314
619
|
|
|
315
620
|
async filterConcept(filterContext, set) {
|
|
316
621
|
|
|
317
622
|
assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext');
|
|
318
|
-
|
|
319
|
-
|
|
623
|
+
|
|
624
|
+
if (set instanceof IETFLanguageEnumerationFilter) {
|
|
625
|
+
const code = set.codes()[set.cursor];
|
|
626
|
+
set.cursor++;
|
|
627
|
+
return this.languageDefinitions.parse(code, {}, LANG_VALIDATION_OPTIONS);
|
|
628
|
+
}
|
|
629
|
+
throw new Issue('error', 'not-supported', null, 'CODESYSTEM_NOT_ENUMERABLE',
|
|
630
|
+
'This filter on urn:ietf:bcp:47 cannot be expanded', 'not-supported', 422);
|
|
320
631
|
}
|
|
321
632
|
|
|
322
633
|
async filterLocate(filterContext, set, code) {
|
|
323
634
|
|
|
324
635
|
assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext');
|
|
325
|
-
assert(set && set
|
|
636
|
+
assert(set && typeof set.matches === 'function', 'set must be a language filter');
|
|
326
637
|
assert(typeof code === 'string', 'code must be non-null string');
|
|
327
638
|
|
|
328
|
-
|
|
639
|
+
// Same rules as locate(), or a code could pass one path and fail the other
|
|
640
|
+
const language = this.languageDefinitions.parse(code, {}, LANG_VALIDATION_OPTIONS);
|
|
329
641
|
if (!language) {
|
|
330
642
|
return `Invalid language code: ${code}`;
|
|
331
643
|
}
|
|
332
644
|
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
switch (filter.component) {
|
|
337
|
-
case LanguageComponent.LANG:
|
|
338
|
-
hasComponent = !!language.language;
|
|
339
|
-
break;
|
|
340
|
-
case LanguageComponent.EXTLANG:
|
|
341
|
-
hasComponent = !!language.extLang.length;
|
|
342
|
-
break;
|
|
343
|
-
case LanguageComponent.SCRIPT:
|
|
344
|
-
hasComponent = !!language.script;
|
|
345
|
-
break;
|
|
346
|
-
case LanguageComponent.REGION:
|
|
347
|
-
hasComponent = !!language.region;
|
|
348
|
-
break;
|
|
349
|
-
case LanguageComponent.VARIANT:
|
|
350
|
-
hasComponent = !!language.variant;
|
|
351
|
-
break;
|
|
352
|
-
case LanguageComponent.EXTENSION:
|
|
353
|
-
hasComponent = !!language.extension;
|
|
354
|
-
break;
|
|
355
|
-
case LanguageComponent.PRIVATE_USE:
|
|
356
|
-
hasComponent = language.privateUse.length > 0;
|
|
357
|
-
break;
|
|
358
|
-
default:
|
|
359
|
-
return `Unknown language component: ${filter.component}`;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (hasComponent === filter.status) {
|
|
363
|
-
return language;
|
|
364
|
-
} else {
|
|
365
|
-
const action = filter.status ? 'does not contain' : 'contains';
|
|
366
|
-
const requirement = filter.status ? 'required' : 'not allowed';
|
|
367
|
-
return `The language code ${code} ${action} a ${filter.component}, and it is ${requirement}`;
|
|
645
|
+
const result = set.matches(language);
|
|
646
|
+
if (typeof result === 'string') {
|
|
647
|
+
return result; // the filter could not be evaluated at all
|
|
368
648
|
}
|
|
649
|
+
return result ? language : set.describe(code, language);
|
|
369
650
|
}
|
|
370
651
|
|
|
652
|
+
|
|
371
653
|
async filterCheck(filterContext, set, concept) {
|
|
372
654
|
|
|
373
655
|
assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext');
|
|
374
|
-
assert(set && set
|
|
656
|
+
assert(set && typeof set.matches === 'function', 'set must be a language filter');
|
|
375
657
|
const ctxt = await this.#ensureContext(concept);
|
|
376
658
|
|
|
377
|
-
|
|
378
|
-
const filter = set;
|
|
379
|
-
let hasComponent = false;
|
|
380
|
-
|
|
381
|
-
switch (filter.component) {
|
|
382
|
-
case LanguageComponent.LANG:
|
|
383
|
-
hasComponent = !!ctxt.language;
|
|
384
|
-
break;
|
|
385
|
-
case LanguageComponent.EXTLANG:
|
|
386
|
-
hasComponent = ctxt.extLang.length > 0;
|
|
387
|
-
break;
|
|
388
|
-
case LanguageComponent.SCRIPT:
|
|
389
|
-
hasComponent = !!ctxt.script;
|
|
390
|
-
break;
|
|
391
|
-
case LanguageComponent.REGION:
|
|
392
|
-
hasComponent = !!ctxt.region;
|
|
393
|
-
break;
|
|
394
|
-
case LanguageComponent.VARIANT:
|
|
395
|
-
hasComponent = !!ctxt.variant;
|
|
396
|
-
break;
|
|
397
|
-
case LanguageComponent.EXTENSION:
|
|
398
|
-
hasComponent = !!ctxt.extension;
|
|
399
|
-
break;
|
|
400
|
-
case LanguageComponent.PRIVATE_USE:
|
|
401
|
-
hasComponent = ctxt.privateUse.length > 0;
|
|
402
|
-
break;
|
|
403
|
-
default:
|
|
404
|
-
return `Unknown language component: ${filter.component}`;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return hasComponent === filter.status;
|
|
659
|
+
return set.matches(ctxt);
|
|
408
660
|
}
|
|
409
661
|
|
|
410
662
|
|
|
@@ -420,12 +672,6 @@ class IETFLanguageCodeProvider extends CodeSystemProvider {
|
|
|
420
672
|
return codeA === codeB;
|
|
421
673
|
}
|
|
422
674
|
|
|
423
|
-
async subsumesTest(codeA, codeB) {
|
|
424
|
-
await this.#ensureContext(codeA);
|
|
425
|
-
await this.#ensureContext(codeB);
|
|
426
|
-
return 'not-subsumed'; // No subsumption in language codes
|
|
427
|
-
}
|
|
428
|
-
|
|
429
675
|
versionAlgorithm() {
|
|
430
676
|
return null;
|
|
431
677
|
}
|
package/tx/cs/cs-loinc.js
CHANGED
|
@@ -1037,12 +1037,53 @@ class LoincServices extends BaseCSServices {
|
|
|
1037
1037
|
return set.hasKey(concept.key);
|
|
1038
1038
|
}
|
|
1039
1039
|
|
|
1040
|
-
|
|
1040
|
+
/**
|
|
1041
|
+
* Subsumption over the LOINC multiaxial hierarchy. The Closure table is a full transitive
|
|
1042
|
+
* ancestor/descendent closure - the importer walks PATH_TO_ROOT from
|
|
1043
|
+
* ComponentHierarchyBySystem and records every ancestor on the path, not just the immediate
|
|
1044
|
+
* parent - so one indexed lookup each way answers the question. This is the same table the
|
|
1045
|
+
* `is-a` / `descendent-of` filters run against, so $subsumes and an expansion agree.
|
|
1046
|
+
*
|
|
1047
|
+
* In practice the relationships found are Part-to-Part and Part-to-code: LOINC codes hang off
|
|
1048
|
+
* the hierarchy as leaves, so two codes never subsume one another.
|
|
1049
|
+
*
|
|
1050
|
+
* @param {string|LoincProviderContext} codeA
|
|
1051
|
+
* @param {string|LoincProviderContext} codeB
|
|
1052
|
+
* @returns {string} equivalent, subsumes, subsumed-by or not-subsumed
|
|
1053
|
+
*/
|
|
1041
1054
|
async subsumesTest(codeA, codeB) {
|
|
1042
|
-
await this.#ensureContext(codeA);
|
|
1043
|
-
await this.#ensureContext(codeB);
|
|
1055
|
+
const a = await this.#ensureContext(codeA);
|
|
1056
|
+
const b = await this.#ensureContext(codeB);
|
|
1057
|
+
|
|
1058
|
+
if (a.key === b.key) {
|
|
1059
|
+
return 'equivalent';
|
|
1060
|
+
}
|
|
1061
|
+
if (await this.#isAncestorOf(a.key, b.key)) {
|
|
1062
|
+
return 'subsumes';
|
|
1063
|
+
}
|
|
1064
|
+
if (await this.#isAncestorOf(b.key, a.key)) {
|
|
1065
|
+
return 'subsumed-by';
|
|
1066
|
+
}
|
|
1067
|
+
return 'not-subsumed';
|
|
1068
|
+
}
|
|
1044
1069
|
|
|
1045
|
-
|
|
1070
|
+
/**
|
|
1071
|
+
* Is ancestorKey an ancestor of descendentKey in the multiaxial hierarchy?
|
|
1072
|
+
* @param {number} ancestorKey
|
|
1073
|
+
* @param {number} descendentKey
|
|
1074
|
+
* @returns {Promise<boolean>}
|
|
1075
|
+
*/
|
|
1076
|
+
async #isAncestorOf(ancestorKey, descendentKey) {
|
|
1077
|
+
return new Promise((resolve, reject) => {
|
|
1078
|
+
const sql = 'SELECT 1 FROM Closure WHERE AncestorKey = ? AND DescendentKey = ? LIMIT 1';
|
|
1079
|
+
this.db.get(sql, [ancestorKey, descendentKey], (err, row) => {
|
|
1080
|
+
if (err) {
|
|
1081
|
+
reject(err);
|
|
1082
|
+
} else {
|
|
1083
|
+
resolve(!!row);
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1046
1087
|
}
|
|
1047
1088
|
|
|
1048
1089
|
versionAlgorithm() {
|