diff --git a/library/languages.js b/library/languages.js index 4cae4e0..f075444 100644 --- a/library/languages.js +++ b/library/languages.js @@ -1,6 +1,7 @@ const fs = require('fs'); const os = require('os'); const {validateOptionalParameter, Utilities} = require("./utilities"); +const {join} = require("node:path"); /** * Language part types for matching depth @@ -22,6 +23,7 @@ class LanguageEntry { constructor() { this.code = ''; this.displays = []; + this.translations = new Map(); // language code -> translated display name } } @@ -497,10 +499,12 @@ class LanguageDefinitions { /** * Load definitions from IETF language subtag registry file */ - static async fromFile(filePath) { + static async fromFiles(filePath) { const definitions = new LanguageDefinitions(); - const content = fs.readFileSync(filePath, 'utf8'); + const content = fs.readFileSync(join(filePath, "lang.dat"), 'utf8'); definitions._load(content); + definitions._loadTranslations(join(filePath, "languages.csv"), definitions.languages); + definitions._loadTranslations(join(filePath, "regions.csv"), definitions.regions); return definitions; } @@ -590,6 +594,88 @@ class LanguageDefinitions { return [vars, i]; } + /** + * Load translations from a CSV file into an existing map of entries. + * CSV format: code,english,french,german,spanish,arabic,chinese,russian,japanese,swahili + * The language codes for the translation columns: + */ + _loadTranslations(csvPath, targetMap) { + if (!fs.existsSync(csvPath)) { + return; + } + const content = fs.readFileSync(csvPath, 'utf8'); + const lines = content.split('\n'); + if (lines.length < 2) return; + + const header = this._parseCsvLine(lines[0]); + // header[0] = 'code', header[1..] = language names mapped to codes + const langCodeMap = { + 'english': 'en', + 'french': 'fr', + 'german': 'de', + 'spanish': 'es', + 'arabic': 'ar', + 'chinese': 'zh', + 'russian': 'ru', + 'japanese': 'ja', + 'swahili': 'sw' + }; + + const columnLangs = header.slice(1).map(h => langCodeMap[h.toLowerCase()] || h.toLowerCase()); + + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) continue; + const fields = this._parseCsvLine(line); + if (fields.length < 2) continue; + + const code = fields[0]; + const entry = targetMap.get(code); + if (!entry) continue; + + for (let j = 1; j < fields.length && j < header.length; j++) { + const langCode = columnLangs[j - 1]; + const value = fields[j]; + if (value) { + entry.translations.set(langCode, value); + } + } + } + } + + /** + * Parse a single CSV line, handling quoted fields with commas + */ + _parseCsvLine(line) { + const fields = []; + let current = ''; + let inQuotes = false; + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (inQuotes) { + if (ch === '"' && i + 1 < line.length && line[i + 1] === '"') { + current += '"'; + i++; + } else if (ch === '"') { + inQuotes = false; + } else { + current += ch; + } + } else { + if (ch === '"') { + inQuotes = true; + } else if (ch === ',') { + fields.push(current); + current = ''; + } else { + current += ch; + } + } + } + fields.push(current); + return fields; + } + /** * Load language entry */ @@ -699,13 +785,29 @@ class LanguageDefinitions { } /** - * Get display name for language + * Get display name for language, optionally translated */ getDisplayForLang(code, displayIndex = 0) { const lang = this.languages.get(code); return lang && lang.displays[displayIndex] ? lang.displays[displayIndex] : code; } + /** + * Get translated display name for a language code + * @param {string} code - the language subtag + * @param {string} displayLang - the language to translate into (e.g. 'fr', 'de') + * @returns {string} translated name, or English name, or the code itself + */ + getTranslatedDisplayForLang(code, displayLang) { + const lang = this.languages.get(code); + if (!lang) return code; + if (displayLang) { + const translated = lang.translations.get(displayLang); + if (translated) return translated; + } + return lang.displays[0] || code; + } + /** * Get display name for region */ @@ -714,6 +816,22 @@ class LanguageDefinitions { return region && region.displays[displayIndex] ? region.displays[displayIndex] : code; } + /** + * Get translated display name for a region code + * @param {string} code - the region subtag + * @param {string} displayLang - the language to translate into (e.g. 'fr', 'de') + * @returns {string} translated name, or English name, or the code itself + */ + getTranslatedDisplayForRegion(code, displayLang) { + const region = this.regions.get(code); + if (!region) return code; + if (displayLang) { + const translated = region.translations.get(displayLang); + if (translated) return translated; + } + return region.displays[0] || code; + } + /** * Get display name for script */ @@ -736,18 +854,21 @@ class LanguageDefinitions { } let result = this.getDisplayForLang(lang.language, displayIndex); - const parts = []; - if (lang.script) { - parts.push(`Script=${this.getDisplayForScript(lang.script, 0)}`); - } - if (lang.region) { - parts.push(`Region=${this.getDisplayForRegion(lang.region, 0)}`); - } - if (lang.variant) { - const variant = this.variants.get(lang.variant); - const variantDisplay = variant && variant.displays[0] ? variant.displays[0] : lang.variant; - parts.push(`Variant=${variantDisplay}`); + if (lang.region && !lang.script && !lang.variant) { + parts.push(`${this.getDisplayForRegion(lang.region, 0)}`); + } else { + if (lang.script) { + parts.push(`Script=${this.getDisplayForScript(lang.script, 0)}`); + } + if (lang.region) { + parts.push(`Region=${this.getDisplayForRegion(lang.region, 0)}`); + } + if (lang.variant) { + const variant = this.variants.get(lang.variant); + const variantDisplay = variant && variant.displays[0] ? variant.displays[0] : lang.variant; + parts.push(`Variant=${variantDisplay}`); + } } if (parts.length > 0) { @@ -778,4 +899,4 @@ module.exports = { LanguageScript, LanguageRegion, LanguageVariant -}; +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9794757..2823160 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { - "name": "node-shl-api", - "version": "0.3.0-SNAPSHOT", + "name": "fhirsmith", + "version": "0.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "node-shl-api", - "version": "0.3.0-SNAPSHOT", - "license": "MIT", + "name": "fhirsmith", + "version": "0.4.2", + "license": "BSD-3", "dependencies": { "axios": "^1.13.4", "base45": "^3.0.0", @@ -46,7 +46,8 @@ "yaml": "^2.8.2" }, "bin": { - "tx-import": "tx-import.js" + "fhirsmith": "server.js", + "tx-import": "tx/importers/tx-import.js" }, "devDependencies": { "@types/jest": "^29.5.8", @@ -1721,9 +1722,9 @@ } }, "node_modules/@types/node": { - "version": "25.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", - "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "devOptional": true, "license": "MIT", "peer": true, @@ -1777,17 +1778,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", - "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", + "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/type-utils": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/type-utils": "8.55.0", + "@typescript-eslint/utils": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -1800,23 +1801,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.54.0", + "@typescript-eslint/parser": "^8.55.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", - "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz", + "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", "debug": "^4.4.3" }, "engines": { @@ -1832,14 +1833,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", - "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz", + "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.54.0", - "@typescript-eslint/types": "^8.54.0", + "@typescript-eslint/tsconfig-utils": "^8.55.0", + "@typescript-eslint/types": "^8.55.0", "debug": "^4.4.3" }, "engines": { @@ -1854,14 +1855,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", - "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", + "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0" + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1872,9 +1873,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", - "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz", + "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==", "dev": true, "license": "MIT", "engines": { @@ -1889,15 +1890,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", - "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", + "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/utils": "8.55.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -1914,9 +1915,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", - "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", + "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", "dev": true, "license": "MIT", "engines": { @@ -1928,16 +1929,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", - "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", + "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.54.0", - "@typescript-eslint/tsconfig-utils": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/project-service": "8.55.0", + "@typescript-eslint/tsconfig-utils": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", @@ -1956,16 +1957,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", - "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", + "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0" + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1980,13 +1981,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", - "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", + "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/types": "8.55.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2272,13 +2273,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -2763,9 +2764,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001767", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", - "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "dev": true, "funding": [ { @@ -4116,9 +4117,9 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", - "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.5.tgz", + "integrity": "sha512-JeaA2Vm9ffQKp9VjvfzObuMCjUYAp5WDYhRYL5LrBPY/jUDlUtOvDfot0vKSkB9tuX885BDHjtw4fZadD95wnA==", "funding": [ { "type": "github", @@ -4127,7 +4128,7 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -4169,9 +4170,9 @@ } }, "node_modules/fhirpath": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/fhirpath/-/fhirpath-4.8.3.tgz", - "integrity": "sha512-BxpESyWyuBnkm9bRy27vMfsnXb4opD5hUviowC9PAkR8qp8HycLwilYX/bfEz1qA5/USLxK+Xx1GUX4lB/1IWw==", + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/fhirpath/-/fhirpath-4.8.5.tgz", + "integrity": "sha512-kzwQ/aQy5OSOpvMkIZW4uLaFQFMCx4DgjQ781uAg3oPvUIca2naU+qUjjukKK2stdYqucMNjl8C66P5Dlx8e6A==", "hasInstallScript": true, "license": "SEE LICENSE in LICENSE.md", "dependencies": { @@ -6671,9 +6672,9 @@ } }, "node_modules/mongoose": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.22.0.tgz", - "integrity": "sha512-LKTPPqD3CVcSZJRzPcwKiSVYTmAvBZeVT0V34vUiqPEo9sBmOEg1y4TpDbUb90Zf2lO4N05ailQnKxiapCN08g==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.0.tgz", + "integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==", "license": "MIT", "dependencies": { "bson": "^6.10.4", @@ -8308,9 +8309,9 @@ "license": "ISC" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/tests/cs/cs-ucum.test.js b/tests/cs/cs-ucum.test.js index e36dec8..dbdbbab 100644 --- a/tests/cs/cs-ucum.test.js +++ b/tests/cs/cs-ucum.test.js @@ -14,7 +14,6 @@ const { } = require('../../tx/cs/cs-api'); const { UcumService } = require('../../tx/library/ucum-service'); -const { Languages, Language } = require('../../library/languages'); const {OperationContext} = require("../../tx/operation-context"); const {TestUtilities} = require("../test-utilities"); @@ -241,7 +240,7 @@ describe('UCUM Provider Integration Tests', () => { const filterContext = new FilterExecutionContext(); // Create filter for mass units (canonical: 'g') - await provider.filter(filterContext, 'canonical', 'equals', 'g'); + await provider.filter(filterContext, 'canonical', '=', 'g'); const filters = await provider.executeFilters(filterContext); // Test units that should match (all mass units) diff --git a/tests/library/languages.test.js b/tests/library/languages.test.js index 2eb6e71..2bc2478 100644 --- a/tests/library/languages.test.js +++ b/tests/library/languages.test.js @@ -277,13 +277,12 @@ describe('Languages Class', () => { describe('LanguageDefinitions Class', () => { let definitions; - beforeAll(() => { + beforeAll(async () => { // Try to load real IETF data, fall back to mock if not available - const realDataPath = path.join(__dirname, '../../tx/data/lang.dat'); + const realDataPath = path.join(__dirname, '../../tx/data'); if (fs.existsSync(realDataPath)) { - const realContent = fs.readFileSync(realDataPath, 'utf8'); - definitions = LanguageDefinitions.fromContent(realContent); + definitions = await LanguageDefinitions.fromFiles(realDataPath); // console.log('Using real IETF language data from lang.dat'); } else { throw new Error('Real data file not found'); @@ -503,7 +502,7 @@ Description: Test test('should present language with region', () => { const lang = new Language('en-US'); const presentation = definitions.present(lang); - expect(presentation).toBe('English (Region=United States)'); + expect(presentation).toBe('English (United States)'); }); test('should present language with script and region', () => { @@ -551,13 +550,12 @@ Description: Test describe('Language System Integration Tests', () => { let definitions; - beforeAll(() => { + beforeAll(async () => { // Try to load real IETF data, fall back to mock if not available - const realDataPath = path.join(__dirname, '../../tx/data/lang.dat'); + const realDataPath = path.join(__dirname, '../../tx/data'); if (fs.existsSync(realDataPath)) { - const realContent = fs.readFileSync(realDataPath, 'utf8'); - definitions = LanguageDefinitions.fromContent(realContent); + definitions = await LanguageDefinitions.fromFiles(realDataPath); // console.log('Using real IETF language data from lang.dat'); } else { throw new Error('Real data file not found'); @@ -598,7 +596,7 @@ describe('Language System Integration Tests', () => { expect(validated.isLangRegion()).toBe(true); const presentation = definitions.present(validated); - expect(presentation).toMatch(/\(Region=/); + expect(presentation).toMatch(/\(/); } }); diff --git a/tests/test-utilities.js b/tests/test-utilities.js index 2bea15e..a42119a 100644 --- a/tests/test-utilities.js +++ b/tests/test-utilities.js @@ -11,7 +11,7 @@ class TestUtilities { static async loadLanguageDefinitions() { if (!this.langDefs) { - this.langDefs = await LanguageDefinitions.fromFile(path.join(__dirname, '../tx/data/lang.dat')); + this.langDefs = await LanguageDefinitions.fromFiles(path.join(__dirname, '../tx/data')); } return this.langDefs; } diff --git a/tests/tx/designations.test.js b/tests/tx/designations.test.js index 2a24b99..d8861b5 100644 --- a/tests/tx/designations.test.js +++ b/tests/tx/designations.test.js @@ -1,5 +1,5 @@ -const fs = require('fs'); const path = require('path'); + const { Designation, Designations, @@ -16,9 +16,8 @@ describe('Designations', () => { beforeAll(async () => { // Load real language registry - const langDataPath = path.join(__dirname, '../../tx/data/lang.dat'); - const content = fs.readFileSync(langDataPath, 'utf8'); - languageDefinitions = LanguageDefinitions.fromContent(content); + const langDataPath = path.join(__dirname, '../../tx/data'); + languageDefinitions = await LanguageDefinitions.fromFiles(langDataPath); }); beforeEach(() => { diff --git a/tests/tx/test-cases.test.js b/tests/tx/test-cases.test.js index 83338bd..211b2cb 100644 --- a/tests/tx/test-cases.test.js +++ b/tests/tx/test-cases.test.js @@ -197,6 +197,42 @@ describe('parameters', () => { await runTest({"suite":"parameters","test":"parameters-expand-isa-property"}); }); + it('parameters-expand-supplement-none', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-supplement-none"}); + }); + + it('parameters-expand-supplement-good', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-supplement-good"}); + }); + + it('parameters-expand-supplement-bad', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-supplement-bad"}); + }); + + it('parameters-validate-supplement-none', async () => { + await runTest({"suite":"parameters","test":"parameters-validate-supplement-none"}); + }); + + it('parameters-validate-supplement-good', async () => { + await runTest({"suite":"parameters","test":"parameters-validate-supplement-good"}); + }); + + it('parameters-validate-supplement-bad', async () => { + await runTest({"suite":"parameters","test":"parameters-validate-supplement-bad"}); + }); + + it('parameters-lookup-supplement-none', async () => { + await runTest({"suite":"parameters","test":"parameters-lookup-supplement-none"}); + }); + + it('parameters-lookup-supplement-good', async () => { + await runTest({"suite":"parameters","test":"parameters-lookup-supplement-good"}); + }); + + it('parameters-lookup-supplement-bad', async () => { + await runTest({"suite":"parameters","test":"parameters-lookup-supplement-bad"}); + }); + }); describe('language', () => { @@ -1816,6 +1852,22 @@ describe('notSelectable', () => { await runTest({"suite":"notSelectable","test":"notSelectable-unprop-false-unknown"}); }); + it('notSelectable-prop-true-true-param-true', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-true-true-param-true"}); + }); + + it('notSelectable-prop-true-true-param-false', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-true-true-param-false"}); + }); + + it('notSelectable-prop-false-false-param-true', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-false-false-param-true"}); + }); + + it('notSelectable-prop-false-false-param-false', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-false-false-param-false"}); + }); + }); describe('inactive', () => { @@ -2340,5 +2392,288 @@ describe('omop', () => { }); + +describe('bugs', () => { + // A series of tests that deal with discovered bugs in FHIRsmith. These tests are specific to FHIRsmith - internal QA + + it('country-codes', async () => { + await runTest({"suite":"bugs","test":"country-codes"}); + }); + + it('no-system', async () => { + await runTest({"suite":"bugs","test":"no-system"}); + }); + + it('sct-parse', async () => { + await runTest({"suite":"bugs","test":"sct-parse"}); + }); + + it('sct-parse-pc', async () => { + await runTest({"suite":"bugs","test":"sct-parse-pc"}); + }); + + it('lang-case', async () => { + await runTest({"suite":"bugs","test":"lang-case"}); + }); + + it('lang-case2', async () => { + await runTest({"suite":"bugs","test":"lang-case2"}); + }); + + it('provenance', async () => { + await runTest({"suite":"bugs","test":"provenance"}); + }); + + it('country-code', async () => { + await runTest({"suite":"bugs","test":"country-code"}); + }); + + it('sct-msg', async () => { + await runTest({"suite":"bugs","test":"sct-msg"}); + }); + + it('sct-display-1', async () => { + await runTest({"suite":"bugs","test":"sct-display-1"}); + }); + + it('sct-display-2', async () => { + await runTest({"suite":"bugs","test":"sct-display-2"}); + }); + + it('x12-bad', async () => { + await runTest({"suite":"bugs","test":"x12-bad"}); + }); + +}); + +describe('permutations', () => { + // A set of permutations generated by Claude with the goal of increasing test coverage. + + it('bad-cc1-all-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-all-request"}); + }); + + it('bad-cc1-enumerated-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-enumerated-request"}); + }); + + it('bad-cc1-exclude-filter-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-exclude-filter-request"}); + }); + + it('bad-cc1-exclude-import-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-exclude-import-request"}); + }); + + it('bad-cc1-exclude-list-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-exclude-list-request"}); + }); + + it('bad-cc1-import-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-import-request"}); + }); + + it('bad-cc1-isa-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-isa-request"}); + }); + + it('bad-cc2-all-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-all-request"}); + }); + + it('bad-cc2-enumerated-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-enumerated-request"}); + }); + + it('bad-cc2-exclude-filter-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-exclude-filter-request"}); + }); + + it('bad-cc2-exclude-import-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-exclude-import-request"}); + }); + + it('bad-cc2-exclude-list-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-exclude-list-request"}); + }); + + it('bad-cc2-import-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-import-request"}); + }); + + it('bad-cc2-isa-request', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-isa-request"}); + }); + + it('bad-coding-all-request', async () => { + await runTest({"suite":"permutations","test":"bad-coding-all-request"}); + }); + + it('bad-coding-enumerated-request', async () => { + await runTest({"suite":"permutations","test":"bad-coding-enumerated-request"}); + }); + + it('bad-coding-exclude-filter-request', async () => { + await runTest({"suite":"permutations","test":"bad-coding-exclude-filter-request"}); + }); + + it('bad-coding-exclude-import-request', async () => { + await runTest({"suite":"permutations","test":"bad-coding-exclude-import-request"}); + }); + + it('bad-coding-exclude-list-request', async () => { + await runTest({"suite":"permutations","test":"bad-coding-exclude-list-request"}); + }); + + it('bad-coding-import-request', async () => { + await runTest({"suite":"permutations","test":"bad-coding-import-request"}); + }); + + it('bad-coding-isa-request', async () => { + await runTest({"suite":"permutations","test":"bad-coding-isa-request"}); + }); + + it('bad-scd-all-request', async () => { + await runTest({"suite":"permutations","test":"bad-scd-all-request"}); + }); + + it('bad-scd-enumerated-request', async () => { + await runTest({"suite":"permutations","test":"bad-scd-enumerated-request"}); + }); + + it('bad-scd-exclude-filter-request', async () => { + await runTest({"suite":"permutations","test":"bad-scd-exclude-filter-request"}); + }); + + it('bad-scd-exclude-import-request', async () => { + await runTest({"suite":"permutations","test":"bad-scd-exclude-import-request"}); + }); + + it('bad-scd-exclude-list-request', async () => { + await runTest({"suite":"permutations","test":"bad-scd-exclude-list-request"}); + }); + + it('bad-scd-import-request', async () => { + await runTest({"suite":"permutations","test":"bad-scd-import-request"}); + }); + + it('bad-scd-isa-request', async () => { + await runTest({"suite":"permutations","test":"bad-scd-isa-request"}); + }); + + it('good-cc1-all-request', async () => { + await runTest({"suite":"permutations","test":"good-cc1-all-request"}); + }); + + it('good-cc1-enumerated-request', async () => { + await runTest({"suite":"permutations","test":"good-cc1-enumerated-request"}); + }); + + it('good-cc1-exclude-filter-request', async () => { + await runTest({"suite":"permutations","test":"good-cc1-exclude-filter-request"}); + }); + + it('good-cc1-exclude-import-request', async () => { + await runTest({"suite":"permutations","test":"good-cc1-exclude-import-request"}); + }); + + it('good-cc1-exclude-list-request', async () => { + await runTest({"suite":"permutations","test":"good-cc1-exclude-list-request"}); + }); + + it('good-cc1-import-request', async () => { + await runTest({"suite":"permutations","test":"good-cc1-import-request"}); + }); + + it('good-cc1-isa-request', async () => { + await runTest({"suite":"permutations","test":"good-cc1-isa-request"}); + }); + + it('good-cc2-all-request', async () => { + await runTest({"suite":"permutations","test":"good-cc2-all-request"}); + }); + + it('good-cc2-enumerated-request', async () => { + await runTest({"suite":"permutations","test":"good-cc2-enumerated-request"}); + }); + + it('good-cc2-exclude-filter-request', async () => { + await runTest({"suite":"permutations","test":"good-cc2-exclude-filter-request"}); + }); + + it('good-cc2-exclude-import-request', async () => { + await runTest({"suite":"permutations","test":"good-cc2-exclude-import-request"}); + }); + + it('good-cc2-exclude-list-request', async () => { + await runTest({"suite":"permutations","test":"good-cc2-exclude-list-request"}); + }); + + it('good-cc2-import-request', async () => { + await runTest({"suite":"permutations","test":"good-cc2-import-request"}); + }); + + it('good-cc2-isa-request', async () => { + await runTest({"suite":"permutations","test":"good-cc2-isa-request"}); + }); + + it('good-coding-all-request', async () => { + await runTest({"suite":"permutations","test":"good-coding-all-request"}); + }); + + it('good-coding-enumerated-request', async () => { + await runTest({"suite":"permutations","test":"good-coding-enumerated-request"}); + }); + + it('good-coding-exclude-filter-request', async () => { + await runTest({"suite":"permutations","test":"good-coding-exclude-filter-request"}); + }); + + it('good-coding-exclude-import-request', async () => { + await runTest({"suite":"permutations","test":"good-coding-exclude-import-request"}); + }); + + it('good-coding-exclude-list-request', async () => { + await runTest({"suite":"permutations","test":"good-coding-exclude-list-request"}); + }); + + it('good-coding-import-request', async () => { + await runTest({"suite":"permutations","test":"good-coding-import-request"}); + }); + + it('good-coding-isa-request', async () => { + await runTest({"suite":"permutations","test":"good-coding-isa-request"}); + }); + + it('good-scd-all-request', async () => { + await runTest({"suite":"permutations","test":"good-scd-all-request"}); + }); + + it('good-scd-enumerated-request', async () => { + await runTest({"suite":"permutations","test":"good-scd-enumerated-request"}); + }); + + it('good-scd-exclude-filter-request', async () => { + await runTest({"suite":"permutations","test":"good-scd-exclude-filter-request"}); + }); + + it('good-scd-exclude-import-request', async () => { + await runTest({"suite":"permutations","test":"good-scd-exclude-import-request"}); + }); + + it('good-scd-exclude-list-request', async () => { + await runTest({"suite":"permutations","test":"good-scd-exclude-list-request"}); + }); + + it('good-scd-import-request', async () => { + await runTest({"suite":"permutations","test":"good-scd-import-request"}); + }); + + it('good-scd-isa-request', async () => { + await runTest({"suite":"permutations","test":"good-scd-isa-request"}); + }); + +}); + }); diff --git a/tests/tx/worker.test.js b/tests/tx/worker.test.js index 10dc4f0..9b7f02b 100644 --- a/tests/tx/worker.test.js +++ b/tests/tx/worker.test.js @@ -242,7 +242,8 @@ describe('TerminologyWorker', () => { version: '1.0.0', name: 'SupplementCS', status: 'active', - supplements: 'http://main.com' + supplements: 'http://main.com', + extension : [{url: 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-type', valueCode: "lang-pack"}] }); worker.additionalResources = [mainCS, supplementCS]; @@ -258,7 +259,8 @@ describe('TerminologyWorker', () => { version: '1.0.0', name: 'SupplementCS', status: 'active', - supplements: 'http://main.com|1.0.0' + supplements: 'http://main.com|1.0.0', + extension : [{url: 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-type', valueCode: "lang-pack"}] }); worker.additionalResources = [supplementCS]; diff --git a/translations/Messages.properties b/translations/Messages.properties index afaeaa2..ff8ffe3 100644 --- a/translations/Messages.properties +++ b/translations/Messages.properties @@ -1312,7 +1312,8 @@ VS_EXP_IMPORT_UNK_PINNED = Unable to find included value set ''{0}'' version ''{ VS_EXP_IMPORT_UNK_PINNED_X = Unable to find excluded value set ''{0}'' version ''{1}'' VS_EXP_IMPORT_UNK_X = Unable to find excluded value set ''{0}'' Wrong_namespace__expected_ = Wrong namespace - expected ''{0}'' -Wrong_type_for_resource = Wrong type for resource +Wrong_type_for_resource = Wrong type for resource +Wrong_type_for_resource_expected = Wrong type for resource. Expected {0} but found {1} TEXT_LINK_DATA_NOT_FOUND = The target of the textLink data reference ''{0}'' was not found in the resource TEXT_LINK_DATA_MULTIPLE_MATCHES = Multiple matching targets for the textLink data reference ''{0}'' were found in the resource XHTML_URL_DATA_DATA_INVALID = The data should be valid base64 content for a data: URL: {0} diff --git a/tx/cs/cs-api.js b/tx/cs/cs-api.js index c5e6e4f..afad7bc 100644 --- a/tx/cs/cs-api.js +++ b/tx/cs/cs-api.js @@ -122,6 +122,17 @@ class CodeSystemProvider { isNotClosed() { return false; } + + /** + * returns true if the code system is case sensitive when comparing codes. + * this is true by default + * + * @returns {boolean} + */ + isCaseSensitive() { + return true; + } + /** * @param {Languages} languages language specification * @returns {boolean} defined properties for the code system diff --git a/tx/cs/cs-cs.js b/tx/cs/cs-cs.js index 3eb1367..e243eac 100644 --- a/tx/cs/cs-cs.js +++ b/tx/cs/cs-cs.js @@ -385,6 +385,10 @@ class FhirCodeSystemProvider extends CodeSystemProvider { return ctxt ? (ctxt.concept.definition || null) : null; } + isCaseSensitive() { + return !this.codeSystem.caseInsensitive(); + } + /** * @param {string|FhirCodeSystemProviderContext} context - Code or context * @returns {Promise} If the concept is abstract diff --git a/tx/cs/cs-lang.js b/tx/cs/cs-lang.js index 54563f6..35d152c 100644 --- a/tx/cs/cs-lang.js +++ b/tx/cs/cs-lang.js @@ -89,7 +89,7 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { // ========== Code Information Methods ========== async code(code) { - + const ctxt = await this.#ensureContext(code); if (ctxt instanceof Language) { return ctxt.code; @@ -98,13 +98,26 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { } async display(code) { - + const ctxt = await this.#ensureContext(code); if (!ctxt) { return null; } - if (this.opContext.langs.isEnglishOrNothing()) { - return this.languageDefinitions.present(ctxt).trim(); + if (!this.opContext.langs.isEnglishOrNothing()) { + // Try translated display for the primary requested language + const primaryLang = this.opContext.langs.getPrimary(); + if (primaryLang && primaryLang.language) { + const langTranslation = this.languageDefinitions.getTranslatedDisplayForLang(ctxt.language, primaryLang.language); + if (langTranslation && langTranslation !== ctxt.language) { + if (ctxt.isLangRegion()) { + const regionTranslation = this.languageDefinitions.getTranslatedDisplayForRegion(ctxt.region, primaryLang.language); + if (regionTranslation && regionTranslation !== ctxt.region) { + return `${langTranslation} (${regionTranslation})`; + } + } + return langTranslation; + } + } } let disp = this._displayFromSupplements(ctxt.code); if (disp) { @@ -114,31 +127,26 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { } async definition(code) { - await this.#ensureContext(code); return null; // No definitions for language codes } async isAbstract(code) { - await this.#ensureContext(code); return false; // Language codes are not abstract } async isInactive(code) { - await this.#ensureContext(code); return false; // We don't track inactive language codes } async isDeprecated(code) { - await this.#ensureContext(code); return false; // We don't track deprecated language codes } async designations(code, displays) { - const ctxt = await this.#ensureContext(code); const designations = []; if (ctxt != null) { @@ -149,8 +157,12 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { const regionDisplay = this.languageDefinitions.getDisplayForRegion(ctxt.region); const regionVariant = `${langDisplay} (${regionDisplay})`; const regionVariant2 = `${langDisplay} (Region=${regionDisplay})`; - displays.addDesignation(false, 'active', 'en', CodeSystem.makeUseForDisplay(), regionVariant); + const regionVariant3 = `${langDisplay}-${regionDisplay}`; + const regionVariant4 = `${langDisplay}-${regionDisplay.toUpperCase()}`; displays.addDesignation(false, 'active', 'en', CodeSystem.makeUseForDisplay(), regionVariant2); + displays.addDesignation(false, 'active', 'en', CodeSystem.makeUseForDisplay(), regionVariant3); + displays.addDesignation(false, 'active', 'en', CodeSystem.makeUseForDisplay(), regionVariant4); + displays.addDesignation(false, 'active', 'en', CodeSystem.makeUseForDisplay(), regionVariant); } // add alternative displays if available const displayCount = this.languageDefinitions.displayCount(ctxt); @@ -167,6 +179,32 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { } } } + // add translated designations from CSV data + const translationLangs = ['fr', 'de', 'es', 'ar', 'zh', 'ru', 'ja', 'sw']; + // languages that don't have upper/lower case distinction + const caselessLangs = new Set(['ar', 'zh', 'ja']); + + for (const tLang of translationLangs) { + const langTranslation = this.languageDefinitions.getTranslatedDisplayForLang(ctxt.language, tLang); + if (langTranslation && langTranslation !== ctxt.language) { + if (ctxt.isLangRegion()) { + const regionTranslation = this.languageDefinitions.getTranslatedDisplayForRegion(ctxt.region, tLang); + if (regionTranslation && regionTranslation !== ctxt.region) { + const translatedDisplay = `${langTranslation} (${regionTranslation})`; + displays.addDesignation(false, 'active', tLang, CodeSystem.makeUseForDisplay(), translatedDisplay); + displays.addDesignation(false, 'active', tLang, CodeSystem.makeUseForDisplay(), `${langTranslation} (Region=${regionTranslation})`); + displays.addDesignation(false, 'active', tLang, CodeSystem.makeUseForDisplay(), `${langTranslation}-${regionTranslation}`); + if (!caselessLangs.has(tLang)) { + displays.addDesignation(false, 'active', tLang, CodeSystem.makeUseForDisplay(), `${langTranslation}-${regionTranslation.toUpperCase()}`); + } + } else { + displays.addDesignation(false, 'active', tLang, CodeSystem.makeUseForDisplay(), langTranslation); + } + } else { + displays.addDesignation(false, 'active', tLang, CodeSystem.makeUseForDisplay(), langTranslation); + } + } + } this._listSupplementDesignations(ctxt.code, displays); } return designations; @@ -194,7 +232,7 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { // ========== Lookup Methods ========== async locate(code) { - + assert(!code || typeof code === 'string', 'code must be string'); if (!code) return { context: null, message: 'Empty code' }; @@ -209,7 +247,7 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { // ========== Filter Methods ========== async doesFilter(prop, op, value) { - + assert(prop != null && typeof prop === 'string', 'prop must be a non-null string'); assert(op != null && typeof op === 'string', 'op must be a non-null string'); assert(value != null && typeof value === 'string', 'value must be a non-null string'); @@ -222,7 +260,7 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { } async searchFilter(filterContext, filter, sort) { - + assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext'); assert(filter && typeof filter === 'string', 'filter must be a non-null string'); assert(typeof sort === 'boolean', 'sort must be a boolean'); @@ -232,7 +270,7 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { async filter(filterContext, prop, op, value) { - + assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext'); assert(prop != null && typeof prop === 'string', 'prop must be a non-null string'); assert(op != null && typeof op === 'string', 'op must be a non-null string'); @@ -258,13 +296,13 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { } async executeFilters(filterContext) { - + assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext'); return filterContext.filters; } async filterSize(filterContext, set) { - + assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext'); assert(set && set instanceof IETFLanguageCodeFilter, 'set must be a IETFLanguageCodeFilter'); @@ -272,27 +310,27 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { } async filtersNotClosed(filterContext) { - + assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext'); return true; // Grammar-based system is not closed } async filterMore(filterContext, set) { - + assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext'); assert(set && set instanceof IETFLanguageCodeFilter, 'set must be a IETFLanguageCodeFilter'); throw new Error('Language valuesets cannot be expanded as they are based on a grammar'); } async filterConcept(filterContext, set) { - + assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext'); assert(set && set instanceof IETFLanguageCodeFilter, 'set must be a IETFLanguageCodeFilter'); throw new Error('Language valuesets cannot be expanded as they are based on a grammar'); } async filterLocate(filterContext, set, code) { - + assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext'); assert(set && set instanceof IETFLanguageCodeFilter, 'set must be a IETFLanguageCodeFilter'); assert(typeof code === 'string', 'code must be non-null string'); @@ -341,7 +379,7 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { } async filterCheck(filterContext, set, concept) { - + assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext'); assert(set && set instanceof IETFLanguageCodeFilter, 'set must be a IETFLanguageCodeFilter'); const ctxt = await this.#ensureContext(concept); @@ -386,7 +424,7 @@ class IETFLanguageCodeProvider extends CodeSystemProvider { // ========== Additional Methods ========== async sameConcept(a, b) { - + const codeA = await this.code(a); const codeB = await this.code(b); return codeA === codeB; diff --git a/tx/cs/cs-ucum.js b/tx/cs/cs-ucum.js index 3c188c1..899d54a 100644 --- a/tx/cs/cs-ucum.js +++ b/tx/cs/cs-ucum.js @@ -251,7 +251,7 @@ class UcumCodeSystemProvider extends CodeSystemProvider { assert(value != null && typeof value === 'string', 'value must be a non-null string'); // Support canonical unit filters - return (prop === 'canonical' && op === 'equals'); + return (prop === 'canonical' && op === '='); } async searchFilter(filterContext, filter, sort) { @@ -285,7 +285,7 @@ class UcumCodeSystemProvider extends CodeSystemProvider { throw new Error(`Unsupported filter property: ${prop}`); } - if (op !== 'equals') { + if (op !== '=') { throw new Error(`Unsupported filter operator for canonical: ${op}`); } diff --git a/tx/data/languages.csv b/tx/data/languages.csv new file mode 100644 index 0000000..0956c32 --- /dev/null +++ b/tx/data/languages.csv @@ -0,0 +1,191 @@ +code,en,fr,de,es,ar,zh,ru,jp,sw +aa,Afar,Afar,Afar,Afar,الأفارية,阿法尔语,Афарский,アファル語,Kiafar +ab,Abkhazian,Abkhaze,Abchasisch,Abjasio,الأبخازية,阿布哈兹语,Абхазский,アブハズ語,Kiabkhazi +ae,Avestan,Avestique,Avestisch,Avéstico,الأفستية,阿维斯陀语,Авестийский,アヴェスタ語,Kiavesta +af,Afrikaans,Afrikaans,Afrikaans,Afrikáans,الأفريقانية,南非荷兰语,Африкаанс,アフリカーンス語,Kiafrikana +ak,Akan,Akan,Akan,Akan,الأكانية,阿肯语,Акан,アカン語,Kiakan +am,Amharic,Amharique,Amharisch,Amárico,الأمهرية,阿姆哈拉语,Амхарский,アムハラ語,Kiamhari +an,Aragonese,Aragonais,Aragonesisch,Aragonés,الأراغونية,阿拉贡语,Арагонский,アラゴン語,Kiaragoni +ar,Arabic,Arabe,Arabisch,Árabe,العربية,阿拉伯语,Арабский,アラビア語,Kiarabu +as,Assamese,Assamais,Assamesisch,Asamés,الأسامية,阿萨姆语,Ассамский,アッサム語,Kiassam +av,Avaric,Avar,Awarisch,Ávaro,الأفارية,阿瓦尔语,Аварский,アヴァル語,Kiavar +ay,Aymara,Aymara,Aymara,Aimara,الأيمارية,艾马拉语,Аймара,アイマラ語,Kiaimara +az,Azerbaijani,Azerbaïdjanais,Aserbaidschanisch,Azerbaiyano,الأذربيجانية,阿塞拜疆语,Азербайджанский,アゼルバイジャン語,Kiazabaijani +ba,Bashkir,Bachkir,Baschkirisch,Baskir,الباشكيرية,巴什基尔语,Башкирский,バシキール語,Kibashkir +be,Belarusian,Biélorusse,Belarussisch,Bielorruso,البيلاروسية,白俄罗斯语,Белорусский,ベラルーシ語,Kibelarusi +bg,Bulgarian,Bulgare,Bulgarisch,Búlgaro,البلغارية,保加利亚语,Болгарский,ブルガリア語,Kibulgaria +bh,Bihari languages,Langues biharies,Biharisch,Lenguas biharies,البيهارية,比哈尔语,Бихарские языки,ビハール語,Lugha za Kibihari +bi,Bislama,Bichlamar,Bislama,Bislama,البيسلامية,比斯拉马语,Бислама,ビスラマ語,Kibislama +bm,Bambara,Bambara,Bambara,Bambara,البامبارية,班巴拉语,Бамбара,バンバラ語,Kibambara +bn,Bengali,Bengali,Bengalisch,Bengalí,البنغالية,孟加拉语,Бенгальский,ベンガル語,Kibengali +bo,Tibetan,Tibétain,Tibetisch,Tibetano,التبتية,藏语,Тибетский,チベット語,Kitibeti +br,Breton,Breton,Bretonisch,Bretón,البريتونية,布列塔尼语,Бретонский,ブルトン語,Kibretoni +bs,Bosnian,Bosnien,Bosnisch,Bosnio,البوسنية,波斯尼亚语,Боснийский,ボスニア語,Kibosnia +ca,Catalan,Catalan,Katalanisch,Catalán,الكتالونية,加泰罗尼亚语,Каталанский,カタルーニャ語,Kikatalani +ce,Chechen,Tchétchène,Tschetschenisch,Checheno,الشيشانية,车臣语,Чеченский,チェチェン語,Kichecheni +ch,Chamorro,Chamorro,Chamorro,Chamorro,التشامورية,查莫罗语,Чаморро,チャモロ語,Kichamoro +co,Corsican,Corse,Korsisch,Corso,الكورسيكية,科西嘉语,Корсиканский,コルシカ語,Kikorsika +cr,Cree,Cri,Cree,Cree,الكرية,克里语,Кри,クリー語,Kikrii +cs,Czech,Tchèque,Tschechisch,Checo,التشيكية,捷克语,Чешский,チェコ語,Kicheki +cu,Church Slavic,Slavon d'église,Kirchenslawisch,Eslavo eclesiástico,السلافية الكنسية,教会斯拉夫语,Церковнославянский,教会スラヴ語,Kislavoni cha Kanisa +cv,Chuvash,Tchouvache,Tschuwaschisch,Chuvasio,التشوفاشية,楚瓦什语,Чувашский,チュヴァシ語,Kichuvash +cy,Welsh,Gallois,Walisisch,Galés,الويلزية,威尔士语,Валлийский,ウェールズ語,Kiwelshi +da,Danish,Danois,Dänisch,Danés,الدانماركية,丹麦语,Датский,デンマーク語,Kidenmaki +de,German,Allemand,Deutsch,Alemán,الألمانية,德语,Немецкий,ドイツ語,Kijerumani +dv,Dhivehi,Divehi,Dhivehi,Divehi,الديفيهية,迪维希语,Мальдивский,ディベヒ語,Kidivehi +dz,Dzongkha,Dzongkha,Dzongkha,Dzongkha,الزونخية,宗卡语,Дзонг-кэ,ゾンカ語,Kidzongkha +ee,Ewe,Éwé,Ewe,Ewé,الإيوية,埃维语,Эве,エウェ語,Kiewe +el,"Modern Greek (1453-)","Grec moderne (après 1453)","Neugriechisch (ab 1453)","Griego moderno (después de 1453)",اليونانية الحديثة,现代希腊语,"Новогреческий (с 1453)",近代ギリシャ語,Kigiriki cha Kisasa +en,English,Anglais,Englisch,Inglés,الإنجليزية,英语,Английский,英語,Kiingereza +eo,Esperanto,Espéranto,Esperanto,Esperanto,الإسبرانتو,世界语,Эсперанто,エスペラント語,Kiesperanto +es,Spanish,Espagnol,Spanisch,Español,الإسبانية,西班牙语,Испанский,スペイン語,Kihispania +et,Estonian,Estonien,Estnisch,Estonio,الإستونية,爱沙尼亚语,Эстонский,エストニア語,Kiestonia +eu,Basque,Basque,Baskisch,Vasco,الباسكية,巴斯克语,Баскский,バスク語,Kibask +fa,Persian,Persan,Persisch,Persa,الفارسية,波斯语,Персидский,ペルシア語,Kiajemi +ff,Fulah,Peul,Fulfulde,Fula,الفولانية,富拉语,Фула,フラ語,Kifulani +fi,Finnish,Finnois,Finnisch,Finlandés,الفنلندية,芬兰语,Финский,フィンランド語,Kifini +fj,Fijian,Fidjien,Fidschi,Fiyiano,الفيجية,斐济语,Фиджийский,フィジー語,Kifiji +fo,Faroese,Féroïen,Färöisch,Feroés,الفاروية,法罗语,Фарерский,フェロー語,Kifaroe +fr,French,Français,Französisch,Francés,الفرنسية,法语,Французский,フランス語,Kifaransa +fy,Western Frisian,Frison occidental,Westfriesisch,Frisón occidental,الفريزية الغربية,西弗里西亚语,Западнофризский,西フリジア語,Kifrisia cha Magharibi +ga,Irish,Irlandais,Irisch,Irlandés,الأيرلندية,爱尔兰语,Ирландский,アイルランド語,Kiayalandi +gd,Scottish Gaelic,Gaélique écossais,Schottisch-Gälisch,Gaélico escocés,الغيلية الاسكتلندية,苏格兰盖尔语,Шотландский гэльский,スコットランド・ゲール語,Kigaeli cha Uskoti +gl,Galician,Galicien,Galicisch,Gallego,الجاليسية,加利西亚语,Галисийский,ガリシア語,Kigalisia +gn,Guarani,Guarani,Guaraní,Guaraní,الغوارانية,瓜拉尼语,Гуарани,グアラニー語,Kiguarani +gu,Gujarati,Goudjarati,Gujarati,Gujarati,الغوجاراتية,古吉拉特语,Гуджарати,グジャラート語,Kigujarati +gv,Manx,Mannois,Manx,Manés,المانكية,马恩语,Мэнский,マン島語,Kimanx +ha,Hausa,Haoussa,Hausa,Hausa,الهوسا,豪萨语,Хауса,ハウサ語,Kihausa +he,Hebrew,Hébreu,Hebräisch,Hebreo,العبرية,希伯来语,Иврит,ヘブライ語,Kiebrania +hi,Hindi,Hindi,Hindi,Hindi,الهندية,印地语,Хинди,ヒンディー語,Kihindi +ho,Hiri Motu,Hiri motu,Hiri-Motu,Hiri motu,الهيري موتو,希里莫图语,Хири-моту,ヒリモツ語,Kihiri Motu +hr,Croatian,Croate,Kroatisch,Croata,الكرواتية,克罗地亚语,Хорватский,クロアチア語,Kikroeshia +ht,Haitian,Haïtien,Haitianisch,Haitiano,الكريولية الهايتية,海地克里奥尔语,Гаитянский,ハイチ語,Kihaiti +hu,Hungarian,Hongrois,Ungarisch,Húngaro,المجرية,匈牙利语,Венгерский,ハンガリー語,Kihungaria +hy,Armenian,Arménien,Armenisch,Armenio,الأرمنية,亚美尼亚语,Армянский,アルメニア語,Kiarmenia +hz,Herero,Héréro,Herero,Herero,الهيريرو,赫雷罗语,Гереро,ヘレロ語,Kiherero +ia,"Interlingua (International Auxiliary Language","Interlingua (langue auxiliaire internationale)","Interlingua (Internationale Plansprache)","Interlingua (Lengua Auxiliar Internacional)",الإنترلنغوة,国际语,Интерлингва,インターリングア,Kiinterlingua +id,Indonesian,Indonésien,Indonesisch,Indonesio,الإندونيسية,印度尼西亚语,Индонезийский,インドネシア語,Kiindonesia +ie,Interlingue,Interlingue,Interlingue,Interlingue,الإنترلينغ,国际语E,Интерлингве,インターリング語,Kiinterlingue +ig,Igbo,Igbo,Igbo,Igbo,الإيغبوية,伊博语,Игбо,イボ語,Kiigbo +ii,Sichuan Yi,Yi du Sichuan,Sichuan-Yi,Yi de Sichuán,اليي السيتشوانية,四川彝语,Сычуаньский и,四川イ語,Kiyi cha Sichuan +ik,Inupiaq,Inupiaq,Inupiaq,Inupiaq,الإينوبياك,伊努皮克语,Инупиак,イヌピアク語,Kiinupiaq +in,Indonesian,Indonésien,Indonesisch,Indonesio,الإندونيسية,印度尼西亚语,Индонезийский,インドネシア語,Kiindonesia +io,Ido,Ido,Ido,Ido,الإيدو,伊多语,Идо,イド語,Kiido +is,Icelandic,Islandais,Isländisch,Islandés,الأيسلندية,冰岛语,Исландский,アイスランド語,Kiaisilandi +it,Italian,Italien,Italienisch,Italiano,الإيطالية,意大利语,Итальянский,イタリア語,Kiitaliano +iu,Inuktitut,Inuktitut,Inuktitut,Inuktitut,الإينوكتيتوت,因纽特语,Инуктитут,イヌクティトゥット語,Kiinuktitut +iw,Hebrew,Hébreu,Hebräisch,Hebreo,العبرية,希伯来语,Иврит,ヘブライ語,Kiebrania +ja,Japanese,Japonais,Japanisch,Japonés,اليابانية,日语,Японский,日本語,Kijapani +ji,Yiddish,Yiddish,Jiddisch,Yidis,اليديشية,意第绪语,Идиш,イディッシュ語,Kiyidishi +jv,Javanese,Javanais,Javanisch,Javanés,الجاوية,爪哇语,Яванский,ジャワ語,Kijava +jw,Javanese,Javanais,Javanisch,Javanés,الجاوية,爪哇语,Яванский,ジャワ語,Kijava +ka,Georgian,Géorgien,Georgisch,Georgiano,الجورجية,格鲁吉亚语,Грузинский,ジョージア語,Kijojia +kg,Kongo,Kongo,Kikongo,Congo,الكونغولية,刚果语,Конго,コンゴ語,Kikongo +ki,Kikuyu,Kikuyu,Kikuyu,Kikuyu,الكيكويو,基库尤语,Кикуйю,キクユ語,Kikikuyu +kj,Kuanyama,Kuanyama,Kwanyama,Kuanyama,الكوانياما,宽亚玛语,Кваньяма,クワニャマ語,Kikuanyama +kk,Kazakh,Kazakh,Kasachisch,Kazajo,الكازاخية,哈萨克语,Казахский,カザフ語,Kikazaki +kl,Kalaallisut,Groenlandais,Grönländisch,Groenlandés,الكالاليسوتية,格陵兰语,Гренландский,グリーンランド語,Kikalaallisut +km,Khmer,Khmer,Khmer,Jemer,الخميرية,高棉语,Кхмерский,クメール語,Kikambodia +kn,Kannada,Kannada,Kannada,Canarés,الكانادية,卡纳达语,Каннада,カンナダ語,Kikannada +ko,Korean,Coréen,Koreanisch,Coreano,الكورية,韩语,Корейский,韓国語,Kikorea +kr,Kanuri,Kanouri,Kanuri,Kanuri,الكانورية,卡努里语,Канури,カヌリ語,Kikanuri +ks,Kashmiri,Kashmiri,Kaschmirisch,Cachemiro,الكشميرية,克什米尔语,Кашмирский,カシミール語,Kikashmiri +ku,Kurdish,Kurde,Kurdisch,Kurdo,الكردية,库尔德语,Курдский,クルド語,Kikurdi +kv,Komi,Komi,Komi,Komi,الكومية,科米语,Коми,コミ語,Kikomi +kw,Cornish,Cornique,Kornisch,Córnico,الكورنية,康沃尔语,Корнский,コーンウォール語,Kikornishi +ky,Kirghiz,Kirghize,Kirgisisch,Kirguís,القيرغيزية,柯尔克孜语,Киргизский,キルギス語,Kikirigizi +la,Latin,Latin,Latein,Latín,اللاتينية,拉丁语,Латинский,ラテン語,Kilatini +lb,Luxembourgish,Luxembourgeois,Luxemburgisch,Luxemburgués,اللوكسمبورغية,卢森堡语,Люксембургский,ルクセンブルク語,Kiluxemburg +lg,Ganda,Ganda,Luganda,Ganda,الغاندية,干达语,Ганда,ガンダ語,Kiganda +li,Limburgan,Limbourgeois,Limburgisch,Limburgués,الليمبورغية,林堡语,Лимбургский,リンブルフ語,Kilimburgi +ln,Lingala,Lingala,Lingala,Lingala,اللينغالية,林加拉语,Лингала,リンガラ語,Kilingala +lo,Lao,Lao,Laotisch,Lao,اللاوسية,老挝语,Лаосский,ラオ語,Kilaosi +lt,Lithuanian,Lituanien,Litauisch,Lituano,الليتوانية,立陶宛语,Литовский,リトアニア語,Kilithuania +lu,Luba-Katanga,Luba-Katanga,Luba-Katanga,Luba-Katanga,اللوبا كاتانغا,卢巴-加丹加语,Луба-катанга,ルバ・カタンガ語,Kiluba-Katanga +lv,Latvian,Letton,Lettisch,Letón,اللاتفية,拉脱维亚语,Латышский,ラトビア語,Kilatvia +mg,Malagasy,Malgache,Malagasy,Malgache,المالاغاسية,马达加斯加语,Малагасийский,マダガスカル語,Kimalagasi +mh,Marshallese,Marshallais,Marshallisch,Marshalés,المارشالية,马绍尔语,Маршалльский,マーシャル語,Kimashali +mi,Maori,Maori,Maori,Maorí,الماورية,毛利语,Маори,マオリ語,Kimaori +mk,Macedonian,Macédonien,Mazedonisch,Macedonio,المقدونية,马其顿语,Македонский,マケドニア語,Kimasedonia +ml,Malayalam,Malayalam,Malayalam,Malayalam,المالايالامية,马拉雅拉姆语,Малаялам,マラヤーラム語,Kimalayalam +mn,Mongolian,Mongol,Mongolisch,Mongol,المنغولية,蒙古语,Монгольский,モンゴル語,Kimongolia +mo,Moldavian,Moldave,Moldauisch,Moldavo,المولدافية,摩尔达维亚语,Молдавский,モルダヴィア語,Kimoldova +mr,Marathi,Marathi,Marathi,Maratí,المراثية,马拉地语,Маратхи,マラーティー語,Kimarathi +ms,"Malay (macrolanguage)","Malais (macrolangue)","Malaiisch (Makrosprache)","Malayo (macrolengua)",الملايوية,马来语,"Малайский (макроязык)",マレー語,Kimalei +mt,Maltese,Maltais,Maltesisch,Maltés,المالطية,马耳他语,Мальтийский,マルタ語,Kimalta +my,Burmese,Birman,Birmanisch,Birmano,البورمية,缅甸语,Бирманский,ビルマ語,Kiburma +na,Nauru,Nauruan,Nauruisch,Nauruano,النورية,瑙鲁语,Науруанский,ナウル語,Kinauru +nb,Norwegian Bokmål,Norvégien bokmål,Norwegisch Bokmål,Noruego bokmål,البوكمالية النرويجية,书面挪威语,Норвежский букмол,ノルウェー語ブークモール,Kinorwe cha Bokmål +nd,North Ndebele,Ndébélé du Nord,Nord-Ndebele,Ndebele del norte,الندبيلية الشمالية,北恩德贝勒语,Северный ндебеле,北ンデベレ語,Kindebele cha Kaskazini +ne,"Nepali (macrolanguage)","Népalais (macrolangue)","Nepalesisch (Makrosprache)","Nepalí (macrolengua)",النيبالية,尼泊尔语,"Непальский (макроязык)",ネパール語,Kinepali +ng,Ndonga,Ndonga,Ndonga,Ndonga,الندونغا,恩敦加语,Ндонга,ンドンガ語,Kindonga +nl,Dutch,Néerlandais,Niederländisch,Neerlandés,الهولندية,荷兰语,Нидерландский,オランダ語,Kiholanzi +nn,Norwegian Nynorsk,Norvégien nynorsk,Norwegisch Nynorsk,Noruego nynorsk,النينورسك النرويجية,新挪威语,Норвежский нюнорск,ノルウェー語ニーノシュク,Kinorwe cha Nynorsk +no,Norwegian,Norvégien,Norwegisch,Noruego,النرويجية,挪威语,Норвежский,ノルウェー語,Kinorwe +nr,South Ndebele,Ndébélé du Sud,Süd-Ndebele,Ndebele del sur,الندبيلية الجنوبية,南恩德贝勒语,Южный ндебеле,南ンデベレ語,Kindebele cha Kusini +nv,Navajo,Navajo,Navajo,Navajo,النافاهو,纳瓦霍语,Навахо,ナバホ語,Kinavajo +ny,Nyanja,Chichewa,Nyanja,Chichewa,النيانجية,齐切瓦语,Ньянджа,チェワ語,Kinyanja +oc,"Occitan (post 1500)","Occitan (après 1500)","Okzitanisch (nach 1500)","Occitano (después de 1500)",الأوكسيتانية,奥克语,"Окситанский (после 1500)",オック語,Kioksitani +oj,Ojibwa,Ojibwa,Ojibwa,Ojibwa,الأوجيبوا,奥吉布瓦语,Оджибва,オジブウェー語,Kiojibwa +om,Oromo,Oromo,Oromo,Oromo,الأورومية,奥罗莫语,Оромо,オロモ語,Kioromo +or,"Oriya (macrolanguage)","Oriya (macrolangue)","Oriya (Makrosprache)","Oriya (macrolengua)",الأوريا,奥里亚语,"Ория (макроязык)",オリヤー語,Kioriya +os,Ossetian,Ossète,Ossetisch,Osetio,الأوسيتية,奥塞梯语,Осетинский,オセット語,Kiosetia +pa,Panjabi,Pendjabi,Panjabi,Panyabí,البنجابية,旁遮普语,Панджаби,パンジャブ語,Kipanjabi +pi,Pali,Pali,Pali,Pali,البالية,巴利语,Пали,パーリ語,Kipali +pl,Polish,Polonais,Polnisch,Polaco,البولندية,波兰语,Польский,ポーランド語,Kipolandi +ps,Pushto,Pachto,Paschtu,Pastún,البشتونية,普什图语,Пушту,パシュトゥー語,Kipashto +pt,Portuguese,Portugais,Portugiesisch,Portugués,البرتغالية,葡萄牙语,Португальский,ポルトガル語,Kireno +qu,Quechua,Quechua,Quechua,Quechua,الكيتشوا,克丘亚语,Кечуа,ケチュア語,Kikechua +rm,Romansh,Romanche,Rätoromanisch,Romanche,الرومانشية,罗曼什语,Романшский,ロマンシュ語,Kiromanshi +rn,Rundi,Rundi,Rundi,Rundi,الرندية,基隆迪语,Рунди,ルンディ語,Kirundi +ro,Romanian,Roumain,Rumänisch,Rumano,الرومانية,罗马尼亚语,Румынский,ルーマニア語,Kiromania +ru,Russian,Russe,Russisch,Ruso,الروسية,俄语,Русский,ロシア語,Kirusi +rw,Kinyarwanda,Kinyarwanda,Kinyarwanda,Kinyarwanda,الكينيارواندية,卢旺达语,Киньяруанда,ルワンダ語,Kinyarwanda +sa,Sanskrit,Sanskrit,Sanskrit,Sánscrito,السنسكريتية,梵语,Санскрит,サンスクリット語,Kisanskriti +sc,Sardinian,Sarde,Sardisch,Sardo,السردينية,萨丁尼亚语,Сардинский,サルデーニャ語,Kisardinia +sd,Sindhi,Sindhi,Sindhi,Sindhi,السندية,信德语,Синдхи,シンド語,Kisindhi +se,Northern Sami,Sami du Nord,Nordsamisch,Sami septentrional,السامية الشمالية,北萨米语,Северносаамский,北サーミ語,Kisami cha Kaskazini +sg,Sango,Sango,Sango,Sango,السانغوية,桑戈语,Санго,サンゴ語,Kisango +sh,Serbo-Croatian,Serbo-croate,Serbokroatisch,Serbocroata,الصربية الكرواتية,塞尔维亚-克罗地亚语,Сербско-хорватский,セルボ・クロアチア語,Kiserbia-Kroatia +si,Sinhala,Cingalais,Singhalesisch,Cingalés,السنهالية,僧伽罗语,Сингальский,シンハラ語,Kisinhala +sk,Slovak,Slovaque,Slowakisch,Eslovaco,السلوفاكية,斯洛伐克语,Словацкий,スロバキア語,Kislovakia +sl,Slovenian,Slovène,Slowenisch,Esloveno,السلوفينية,斯洛文尼亚语,Словенский,スロベニア語,Kislovenia +sm,Samoan,Samoan,Samoanisch,Samoano,الساموائية,萨摩亚语,Самоанский,サモア語,Kisamoa +sn,Shona,Shona,Shona,Shona,الشونية,绍纳语,Шона,ショナ語,Kishona +so,Somali,Somali,Somali,Somalí,الصومالية,索马里语,Сомалийский,ソマリ語,Kisomali +sq,Albanian,Albanais,Albanisch,Albanés,الألبانية,阿尔巴尼亚语,Албанский,アルバニア語,Kialbenia +sr,Serbian,Serbe,Serbisch,Serbio,الصربية,塞尔维亚语,Сербский,セルビア語,Kiserbia +ss,Swati,Swati,Swati,Suazi,السوازية,斯瓦蒂语,Свати,スワジ語,Kiswati +st,Southern Sotho,Sotho du Sud,Süd-Sotho,Sesotho meridional,السوتو الجنوبية,南索托语,Южный сото,南ソト語,Kisotho cha Kusini +su,Sundanese,Soundanais,Sundanesisch,Sundanés,السوندانية,巽他语,Сунданский,スンダ語,Kisunda +sv,Swedish,Suédois,Schwedisch,Sueco,السويدية,瑞典语,Шведский,スウェーデン語,Kiswidi +sw,"Swahili (macrolanguage)","Swahili (macrolangue)","Swahili (Makrosprache)","Suajili (macrolengua)",السواحيلية,斯瓦希里语,"Суахили (макроязык)",スワヒリ語,Kiswahili +ta,Tamil,Tamoul,Tamil,Tamil,التاميلية,泰米尔语,Тамильский,タミル語,Kitamili +te,Telugu,Télougou,Telugu,Telugu,التيلوغوية,泰卢固语,Телугу,テルグ語,Kitelugu +tg,Tajik,Tadjik,Tadschikisch,Tayiko,الطاجيكية,塔吉克语,Таджикский,タジク語,Kitajiki +th,Thai,Thaï,Thai,Tailandés,التايلاندية,泰语,Тайский,タイ語,Kithai +ti,Tigrinya,Tigrigna,Tigrinisch,Tigriña,التغرينية,提格利尼亚语,Тигринья,ティグリニア語,Kitigrinya +tk,Turkmen,Turkmène,Turkmenisch,Turcomano,التركمانية,土库曼语,Туркменский,トルクメン語,Kiturukimeni +tl,Tagalog,Tagalog,Tagalog,Tagalo,التاغالوغية,他加禄语,Тагальский,タガログ語,Kitagalogi +tn,Tswana,Tswana,Setswana,Setsuana,التسوانية,茨瓦纳语,Тсвана,ツワナ語,Kitswana +to,"Tonga (Tonga Islands)","Tonga (Îles Tonga)","Tongaisch (Tongainseln)","Tonga (Islas Tonga)",التونغوية,汤加语,Тонганский,トンガ語,Kitonga +tr,Turkish,Turc,Türkisch,Turco,التركية,土耳其语,Турецкий,トルコ語,Kituruki +ts,Tsonga,Tsonga,Tsonga,Tsonga,التسونغية,聪加语,Тсонга,ツォンガ語,Kitsonga +tt,Tatar,Tatar,Tatarisch,Tártaro,التتارية,鞑靼语,Татарский,タタール語,Kitatari +tw,Twi,Twi,Twi,Twi,التوية,契维语,Тви,トウィ語,Kitwi +ty,Tahitian,Tahitien,Tahitisch,Tahitiano,التاهيتية,塔希提语,Таитянский,タヒチ語,Kitahiti +ug,Uighur,Ouïgour,Uigurisch,Uigur,الأويغورية,维吾尔语,Уйгурский,ウイグル語,Kiuygur +uk,Ukrainian,Ukrainien,Ukrainisch,Ucraniano,الأوكرانية,乌克兰语,Украинский,ウクライナ語,Kiukraini +ur,Urdu,Ourdou,Urdu,Urdu,الأردية,乌尔都语,Урду,ウルドゥー語,Kiurdu +uz,Uzbek,Ouzbek,Usbekisch,Uzbeko,الأوزبكية,乌兹别克语,Узбекский,ウズベク語,Kiuzbeki +ve,Venda,Venda,Venda,Venda,الفيندية,文达语,Венда,ベンダ語,Kivenda +vi,Vietnamese,Vietnamien,Vietnamesisch,Vietnamita,الفيتنامية,越南语,Вьетнамский,ベトナム語,Kivietinamu +vo,Volapük,Volapük,Volapük,Volapük,الفولابوكية,沃拉普克语,Волапюк,ヴォラピュク語,Kivolapük +wa,Walloon,Wallon,Wallonisch,Valón,الوالونية,瓦隆语,Валлонский,ワロン語,Kiwaluni +wo,Wolof,Wolof,Wolof,Wólof,الولوفية,沃洛夫语,Волоф,ウォロフ語,Kiwolof +xh,Xhosa,Xhosa,Xhosa,Xhosa,الكوسا,科萨语,Коса,コサ語,Kikosa +yi,Yiddish,Yiddish,Jiddisch,Yidis,اليديشية,意第绪语,Идиш,イディッシュ語,Kiyidishi +yo,Yoruba,Yoruba,Yoruba,Yoruba,اليوروبا,约鲁巴语,Йоруба,ヨルバ語,Kiyoruba +za,Zhuang,Zhuang,Zhuang,Zhuang,الزوانغية,壮语,Чжуанский,チワン語,Kizhuang +zh,Chinese,Chinois,Chinesisch,Chino,الصينية,中文,Китайский,中国語,Kichina +zu,Zulu,Zoulou,Zulu,Zulú,الزولو,祖鲁语,Зулусский,ズールー語,Kizulu \ No newline at end of file diff --git a/tx/data/regions.csv b/tx/data/regions.csv new file mode 100644 index 0000000..3f99ad5 --- /dev/null +++ b/tx/data/regions.csv @@ -0,0 +1,273 @@ +code,en,fr,de,es,ar,zh,ru,jp,sw +AA,Private use,Usage privé,Privatnutzung,Uso privado,استخدام خاص,私人使用,Частное использование,私用,Matumizi binafsi +AC,Ascension Island,Île de l'Ascension,Ascension,Isla Ascensión,جزيرة أسنسيون,阿森松岛,Остров Вознесения,アセンション島,Kisiwa cha Ascension +AD,Andorra,Andorre,Andorra,Andorra,أندورا,安道尔,Андорра,アンドラ,Andora +AE,United Arab Emirates,Émirats arabes unis,Vereinigte Arabische Emirate,Emiratos Árabes Unidos,الإمارات العربية المتحدة,阿拉伯联合酋长国,Объединённые Арабские Эмираты,アラブ首長国連邦,Falme za Kiarabu +AF,Afghanistan,Afghanistan,Afghanistan,Afganistán,أفغانستان,阿富汗,Афганистан,アフガニスタン,Afghanistan +AG,Antigua and Barbuda,Antigua-et-Barbuda,Antigua und Barbuda,Antigua y Barbuda,أنتيغوا وبربودا,安提瓜和巴布达,Антигуа и Барбуда,アンティグア・バーブーダ,Antigua na Barbuda +AI,Anguilla,Anguilla,Anguilla,Anguila,أنغيلا,安圭拉,Ангилья,アンギラ,Anguilla +AL,Albania,Albanie,Albanien,Albania,ألبانيا,阿尔巴尼亚,Албания,アルバニア,Albania +AM,Armenia,Arménie,Armenien,Armenia,أرمينيا,亚美尼亚,Армения,アルメニア,Armenia +AN,Netherlands Antilles,Antilles néerlandaises,Niederländische Antillen,Antillas Neerlandesas,جزر الأنتيل الهولندية,荷属安的列斯,Нидерландские Антильские острова,オランダ領アンティル,Antili za Kiholanzi +AO,Angola,Angola,Angola,Angola,أنغولا,安哥拉,Ангола,アンゴラ,Angola +AQ,Antarctica,Antarctique,Antarktis,Antártida,أنتاركتيكا,南极洲,Антарктида,南極大陸,Antaktika +AR,Argentina,Argentine,Argentinien,Argentina,الأرجنتين,阿根廷,Аргентина,アルゼンチン,Ajentina +AS,American Samoa,Samoa américaines,Amerikanisch-Samoa,Samoa Americana,ساموا الأمريكية,美属萨摩亚,Американское Самоа,米領サモア,Samoa ya Marekani +AT,Austria,Autriche,Österreich,Austria,النمسا,奥地利,Австрия,オーストリア,Austria +AU,Australia,Australie,Australien,Australia,أستراليا,澳大利亚,Австралия,オーストラリア,Australia +AW,Aruba,Aruba,Aruba,Aruba,أروبا,阿鲁巴,Аруба,アルバ,Aruba +AX,Åland Islands,Îles Åland,Ålandinseln,Islas Åland,جزر آلاند,奥兰群岛,Аландские острова,オーランド諸島,Visiwa vya Åland +AZ,Azerbaijan,Azerbaïdjan,Aserbaidschan,Azerbaiyán,أذربيجان,阿塞拜疆,Азербайджан,アゼルバイジャン,Azabajani +BA,Bosnia and Herzegovina,Bosnie-Herzégovine,Bosnien und Herzegowina,Bosnia y Herzegovina,البوسنة والهرسك,波斯尼亚和黑塞哥维那,Босния и Герцеговина,ボスニア・ヘルツェゴビナ,Bosnia na Herzegovina +BB,Barbados,Barbade,Barbados,Barbados,بربادوس,巴巴多斯,Барбадос,バルバドス,Babados +BD,Bangladesh,Bangladesh,Bangladesch,Bangladés,بنغلاديش,孟加拉国,Бангладеш,バングラデシュ,Bangladeshi +BE,Belgium,Belgique,Belgien,Bélgica,بلجيكا,比利时,Бельгия,ベルギー,Ubelgiji +BF,Burkina Faso,Burkina Faso,Burkina Faso,Burkina Faso,بوركينا فاسو,布基纳法索,Буркина-Фасо,ブルキナファソ,Burkina Faso +BG,Bulgaria,Bulgarie,Bulgarien,Bulgaria,بلغاريا,保加利亚,Болгария,ブルガリア,Bulgaria +BH,Bahrain,Bahreïn,Bahrain,Baréin,البحرين,巴林,Бахрейн,バーレーン,Bahareni +BI,Burundi,Burundi,Burundi,Burundi,بوروندي,布隆迪,Бурунди,ブルンジ,Burundi +BJ,Benin,Bénin,Benin,Benín,بنين,贝宁,Бенин,ベナン,Benini +BL,Saint Barthélemy,Saint-Barthélemy,Saint-Barthélemy,San Bartolomé,سان بارتليمي,圣巴泰勒米,Сен-Бартелеми,サン・バルテルミー,Saint Barthélemy +BM,Bermuda,Bermudes,Bermuda,Bermudas,برمودا,百慕大,Бермудские острова,バミューダ,Bermuda +BN,Brunei Darussalam,Brunei Darussalam,Brunei Darussalam,Brunéi Darussalam,بروناي,文莱,Бруней-Даруссалам,ブルネイ,Brunei +BO,Bolivia,Bolivie,Bolivien,Bolivia,بوليفيا,玻利维亚,Боливия,ボリビア,Bolivia +"BQ","Bonaire, Sint Eustatius and Saba","Bonaire, Saint-Eustache et Saba","Bonaire, Sint Eustatius und Saba","Bonaire, San Eustaquio y Saba",بونير وسانت يوستاتيوس وسابا,博奈尔圣尤斯特歇斯和萨巴,"Бонэйр, Синт-Эстатиус и Саба",ボネール・シント・ユースタティウス・サバ,Bonaire Sint Eustatius na Saba +BR,Brazil,Brésil,Brasilien,Brasil,البرازيل,巴西,Бразилия,ブラジル,Brazili +BS,Bahamas,Bahamas,Bahamas,Bahamas,جزر البهاما,巴哈马,Багамские Острова,バハマ,Bahama +BT,Bhutan,Bhoutan,Bhutan,Bután,بوتان,不丹,Бутан,ブータン,Bhutani +BU,Burma,Birmanie,Birma,Birmania,بورما,缅甸,Бирма,ビルマ,Burma +BV,Bouvet Island,Île Bouvet,Bouvetinsel,Isla Bouvet,جزيرة بوفيه,布韦岛,Остров Буве,ブーベ島,Kisiwa cha Bouvet +BW,Botswana,Botswana,Botsuana,Botsuana,بوتسوانا,博茨瓦纳,Ботсвана,ボツワナ,Botswana +BY,Belarus,Biélorussie,Belarus,Bielorrusia,بيلاروس,白俄罗斯,Беларусь,ベラルーシ,Belarusi +BZ,Belize,Belize,Belize,Belice,بليز,伯利兹,Белиз,ベリーズ,Belize +CA,Canada,Canada,Kanada,Canadá,كندا,加拿大,Канада,カナダ,Kanada +CC,Cocos (Keeling) Islands,Îles Cocos (Keeling),Kokosinseln (Keelinginseln),Islas Cocos (Keeling),جزر كوكوس,科科斯(基林)群岛,Кокосовые острова,ココス(キーリング)諸島,Visiwa vya Cocos +CD,The Democratic Republic of the Congo,République démocratique du Congo,Demokratische Republik Kongo,República Democrática del Congo,جمهورية الكونغو الديمقراطية,刚果民主共和国,Демократическая Республика Конго,コンゴ民主共和国,Jamhuri ya Kidemokrasia ya Kongo +CF,Central African Republic,République centrafricaine,Zentralafrikanische Republik,República Centroafricana,جمهورية أفريقيا الوسطى,中非共和国,Центральноафриканская Республика,中央アフリカ共和国,Jamhuri ya Afrika ya Kati +CG,Congo,Congo,Kongo,Congo,الكونغو,刚果,Конго,コンゴ共和国,Kongo +CH,Switzerland,Suisse,Schweiz,Suiza,سويسرا,瑞士,Швейцария,スイス,Uswisi +CI,Côte d'Ivoire,Côte d'Ivoire,Côte d'Ivoire,Costa de Marfil,ساحل العاج,科特迪瓦,Кот-д'Ивуар,コートジボワール,Cote d'Ivoire +CK,Cook Islands,Îles Cook,Cookinseln,Islas Cook,جزر كوك,库克群岛,Острова Кука,クック諸島,Visiwa vya Cook +CL,Chile,Chili,Chile,Chile,تشيلي,智利,Чили,チリ,Chile +CM,Cameroon,Cameroun,Kamerun,Camerún,الكاميرون,喀麦隆,Камерун,カメルーン,Kameruni +CN,China,Chine,China,China,الصين,中国,Китай,中国,China +CO,Colombia,Colombie,Kolumbien,Colombia,كولومبيا,哥伦比亚,Колумбия,コロンビア,Kolombia +CP,Clipperton Island,Île Clipperton,Clippertoninsel,Isla Clipperton,جزيرة كليبرتون,克利珀顿岛,Остров Клиппертон,クリッパートン島,Kisiwa cha Clipperton +CQ,Sark,Sercq,Sark,Sark,سارك,萨克岛,Сарк,サーク島,Sark +CR,Costa Rica,Costa Rica,Costa Rica,Costa Rica,كوستاريكا,哥斯达黎加,Коста-Рика,コスタリカ,Kostarika +CS,Serbia and Montenegro,Serbie-et-Monténégro,Serbien und Montenegro,Serbia y Montenegro,صربيا والجبل الأسود,塞尔维亚和黑山,Сербия и Черногория,セルビア・モンテネグロ,Serbia na Montenegro +CU,Cuba,Cuba,Kuba,Cuba,كوبا,古巴,Куба,キューバ,Kuba +CV,Cabo Verde,Cabo Verde,Kap Verde,Cabo Verde,الرأس الأخضر,佛得角,Кабо-Верде,カーボベルデ,Cabo Verde +CW,Curaçao,Curaçao,Curaçao,Curazao,كوراساو,库拉索,Кюрасао,キュラソー,Curaçao +CX,Christmas Island,Île Christmas,Weihnachtsinsel,Isla de Navidad,جزيرة عيد الميلاد,圣诞岛,Остров Рождества,クリスマス島,Kisiwa cha Krismasi +CY,Cyprus,Chypre,Zypern,Chipre,قبرص,塞浦路斯,Кипр,キプロス,Kupro +CZ,Czechia,Tchéquie,Tschechien,Chequia,التشيك,捷克,Чехия,チェコ,Ucheki +DD,German Democratic Republic,République démocratique allemande,Deutsche Demokratische Republik,República Democrática Alemana,جمهورية ألمانيا الديمقراطية,德意志民主共和国,Германская Демократическая Республика,ドイツ民主共和国,Jamhuri ya Kidemokrasia ya Ujerumani +DE,Germany,Allemagne,Deutschland,Alemania,ألمانيا,德国,Германия,ドイツ,Ujerumani +DG,Diego Garcia,Diego Garcia,Diego Garcia,Diego García,دييغو غارسيا,迪戈加西亚岛,Диего-Гарсия,ディエゴガルシア,Diego Garcia +DJ,Djibouti,Djibouti,Dschibuti,Yibuti,جيبوتي,吉布提,Джибути,ジブチ,Jibuti +DK,Denmark,Danemark,Dänemark,Dinamarca,الدنمارك,丹麦,Дания,デンマーク,Denmaki +DM,Dominica,Dominique,Dominica,Dominica,دومينيكا,多米尼克,Доминика,ドミニカ国,Dominika +DO,Dominican Republic,République dominicaine,Dominikanische Republik,República Dominicana,جمهورية الدومينيكان,多米尼加共和国,Доминиканская Республика,ドミニカ共和国,Jamhuri ya Dominika +DZ,Algeria,Algérie,Algerien,Argelia,الجزائر,阿尔及利亚,Алжир,アルジェリア,Aljeria +"EA","Ceuta, Melilla",Ceuta et Melilla,Ceuta und Melilla,Ceuta y Melilla,سبتة ومليلية,休达和梅利利亚,Сеута и Мелилья,セウタ・メリリャ,Ceuta na Melilla +EC,Ecuador,Équateur,Ecuador,Ecuador,الإكوادور,厄瓜多尔,Эквадор,エクアドル,Ekwado +EE,Estonia,Estonie,Estland,Estonia,إستونيا,爱沙尼亚,Эстония,エストニア,Estonia +EG,Egypt,Égypte,Ägypten,Egipto,مصر,埃及,Египет,エジプト,Misri +EH,Western Sahara,Sahara occidental,Westsahara,Sáhara Occidental,الصحراء الغربية,西撒哈拉,Западная Сахара,西サハラ,Sahara Magharibi +ER,Eritrea,Érythrée,Eritrea,Eritrea,إريتريا,厄立特里亚,Эритрея,エリトリア,Eritrea +ES,Spain,Espagne,Spanien,España,إسبانيا,西班牙,Испания,スペイン,Uhispania +ET,Ethiopia,Éthiopie,Äthiopien,Etiopía,إثيوبيا,埃塞俄比亚,Эфиопия,エチオピア,Uhabeshi +EU,European Union,Union européenne,Europäische Union,Unión Europea,الاتحاد الأوروبي,欧盟,Европейский союз,欧州連合,Umoja wa Ulaya +EZ,Eurozone,Zone euro,Eurozone,Eurozona,منطقة اليورو,欧元区,Еврозона,ユーロ圏,Eneo la Euro +FI,Finland,Finlande,Finnland,Finlandia,فنلندا,芬兰,Финляндия,フィンランド,Ufini +FJ,Fiji,Fidji,Fidschi,Fiyi,فيجي,斐济,Фиджи,フィジー,Fiji +FK,Falkland Islands (Malvinas),Îles Malouines (Falkland),Falklandinseln (Malwinen),Islas Malvinas (Falkland),جزر فوكلاند,福克兰群岛,Фолклендские острова,フォークランド諸島,Visiwa vya Falkland +FM,Federated States of Micronesia,États fédérés de Micronésie,Föderierte Staaten von Mikronesien,Estados Federados de Micronesia,ولايات ميكرونيزيا المتحدة,密克罗尼西亚联邦,Федеративные Штаты Микронезии,ミクロネシア連邦,Mikronesia +FO,Faroe Islands,Îles Féroé,Färöer,Islas Feroe,جزر فارو,法罗群岛,Фарерские острова,フェロー諸島,Visiwa vya Faroe +FR,France,France,Frankreich,Francia,فرنسا,法国,Франция,フランス,Ufaransa +FX,Metropolitan France,France métropolitaine,Metropolitan-Frankreich,Francia metropolitana,فرنسا الأوروبية,法国本土,Метрополия Франция,フランス本土,Ufaransa Bara +GA,Gabon,Gabon,Gabun,Gabón,الغابون,加蓬,Габон,ガボン,Gaboni +GB,United Kingdom,Royaume-Uni,Vereinigtes Königreich,Reino Unido,المملكة المتحدة,英国,Великобритания,イギリス,Uingereza +GD,Grenada,Grenade,Grenada,Granada,غرينادا,格林纳达,Гренада,グレナダ,Grenada +GE,Georgia,Géorgie,Georgien,Georgia,جورجيا,格鲁吉亚,Грузия,ジョージア,Jojia +GF,French Guiana,Guyane française,Französisch-Guayana,Guayana Francesa,غويانا الفرنسية,法属圭亚那,Французская Гвиана,仏領ギアナ,Gwiyana ya Ufaransa +GG,Guernsey,Guernesey,Guernsey,Guernsey,غيرنزي,根西岛,Гернси,ガーンジー,Guernsey +GH,Ghana,Ghana,Ghana,Ghana,غانا,加纳,Гана,ガーナ,Ghana +GI,Gibraltar,Gibraltar,Gibraltar,Gibraltar,جبل طارق,直布罗陀,Гибралтар,ジブラルタル,Jibralta +GL,Greenland,Groenland,Grönland,Groenlandia,غرينلاند,格陵兰,Гренландия,グリーンランド,Grinlandi +GM,Gambia,Gambie,Gambia,Gambia,غامبيا,冈比亚,Гамбия,ガンビア,Gambia +GN,Guinea,Guinée,Guinea,Guinea,غينيا,几内亚,Гвинея,ギニア,Gine +GP,Guadeloupe,Guadeloupe,Guadeloupe,Guadalupe,غوادلوب,瓜德罗普,Гваделупа,グアドループ,Gwadelupe +GQ,Equatorial Guinea,Guinée équatoriale,Äquatorialguinea,Guinea Ecuatorial,غينيا الاستوائية,赤道几内亚,Экваториальная Гвинея,赤道ギニア,Gine ya Ikweta +GR,Greece,Grèce,Griechenland,Grecia,اليونان,希腊,Греция,ギリシャ,Ugiriki +GS,South Georgia and the South Sandwich Islands,Géorgie du Sud-et-les Îles Sandwich du Sud,Südgeorgien und die Südlichen Sandwichinseln,Georgia del Sur y las Islas Sandwich del Sur,جورجيا الجنوبية وجزر ساندويتش الجنوبية,南乔治亚和南桑威奇群岛,Южная Георгия и Южные Сандвичевы Острова,サウスジョージア・サウスサンドウィッチ諸島,Jojia Kusini na Visiwa vya Sandwich Kusini +GT,Guatemala,Guatemala,Guatemala,Guatemala,غواتيمالا,危地马拉,Гватемала,グアテマラ,Guatemala +GU,Guam,Guam,Guam,Guam,غوام,关岛,Гуам,グアム,Guam +GW,Guinea-Bissau,Guinée-Bissau,Guinea-Bissau,Guinea-Bisáu,غينيا بيساو,几内亚比绍,Гвинея-Бисау,ギニアビサウ,Gine Bisau +GY,Guyana,Guyana,Guyana,Guyana,غيانا,圭亚那,Гайана,ガイアナ,Guyana +HK,Hong Kong,Hong Kong,Hongkong,Hong Kong,هونغ كونغ,香港,Гонконг,香港,Hong Kong +HM,Heard Island and McDonald Islands,Îles Heard-et-MacDonald,Heard und McDonaldinseln,Islas Heard y McDonald,جزيرة هيرد وجزر ماكدونالد,赫德岛和麦克唐纳群岛,Остров Херд и Острова Макдональд,ハード島・マクドナルド諸島,Kisiwa cha Heard na Visiwa vya McDonald +HN,Honduras,Honduras,Honduras,Honduras,هندوراس,洪都拉斯,Гондурас,ホンジュラス,Hondurasi +HR,Croatia,Croatie,Kroatien,Croacia,كرواتيا,克罗地亚,Хорватия,クロアチア,Kroeshia +HT,Haiti,Haïti,Haiti,Haití,هايتي,海地,Гаити,ハイチ,Haiti +HU,Hungary,Hongrie,Ungarn,Hungría,المجر,匈牙利,Венгрия,ハンガリー,Hungaria +IC,Canary Islands,Îles Canaries,Kanarische Inseln,Islas Canarias,جزر الكناري,加那利群岛,Канарские острова,カナリア諸島,Visiwa vya Kanari +ID,Indonesia,Indonésie,Indonesien,Indonesia,إندونيسيا,印度尼西亚,Индонезия,インドネシア,Indonesia +IE,Ireland,Irlande,Irland,Irlanda,أيرلندا,爱尔兰,Ирландия,アイルランド,Ayalandi +IL,Israel,Israël,Israel,Israel,إسرائيل,以色列,Израиль,イスラエル,Israeli +IM,Isle of Man,Île de Man,Isle of Man,Isla de Man,جزيرة مان,马恩岛,Остров Мэн,マン島,Kisiwa cha Man +IN,India,Inde,Indien,India,الهند,印度,Индия,インド,India +IO,British Indian Ocean Territory,Territoire britannique de l'océan Indien,Britisches Territorium im Indischen Ozean,Territorio Británico del Océano Índico,إقليم المحيط الهندي البريطاني,英属印度洋领地,Британская территория в Индийском океане,英領インド洋地域,Eneo la Uingereza katika Bahari Hindi +IQ,Iraq,Irak,Irak,Irak,العراق,伊拉克,Ирак,イラク,Iraki +IR,Islamic Republic of Iran,République islamique d'Iran,Islamische Republik Iran,República Islámica de Irán,جمهورية إيران الإسلامية,伊朗伊斯兰共和国,Исламская Республика Иран,イラン・イスラム共和国,Jamhuri ya Kiislamu ya Iran +IS,Iceland,Islande,Island,Islandia,آيسلندا,冰岛,Исландия,アイスランド,Aisilandi +IT,Italy,Italie,Italien,Italia,إيطاليا,意大利,Италия,イタリア,Italia +JE,Jersey,Jersey,Jersey,Jersey,جيرزي,泽西岛,Джерси,ジャージー,Jersey +JM,Jamaica,Jamaïque,Jamaika,Jamaica,جامايكا,牙买加,Ямайка,ジャマイカ,Jamaika +JO,Jordan,Jordanie,Jordanien,Jordania,الأردن,约旦,Иордания,ヨルダン,Jordani +JP,Japan,Japon,Japan,Japón,اليابان,日本,Япония,日本,Japani +KE,Kenya,Kenya,Kenia,Kenia,كينيا,肯尼亚,Кения,ケニア,Kenya +KG,Kyrgyzstan,Kirghizistan,Kirgisistan,Kirguistán,قيرغيزستان,吉尔吉斯斯坦,Киргизия,キルギス,Kirigizistani +KH,Cambodia,Cambodge,Kambodscha,Camboya,كمبوديا,柬埔寨,Камбоджа,カンボジア,Kambodia +KI,Kiribati,Kiribati,Kiribati,Kiribati,كيريباتي,基里巴斯,Кирибати,キリバス,Kiribati +KM,Comoros,Comores,Komoren,Comoras,جزر القمر,科摩罗,Коморские Острова,コモロ,Komoro +KN,Saint Kitts and Nevis,Saint-Kitts-et-Nevis,St. Kitts und Nevis,San Cristóbal y Nieves,سانت كيتس ونيفيس,圣基茨和尼维斯,Сент-Китс и Невис,セントクリストファー・ネイビス,Saint Kitts na Nevis +KP,Democratic People's Republic of Korea,République populaire démocratique de Corée,Demokratische Volksrepublik Korea,República Popular Democrática de Corea,جمهورية كوريا الشعبية الديمقراطية,朝鲜民主主义人民共和国,Корейская Народно-Демократическая Республика,朝鮮民主主義人民共和国,Jamhuri ya Watu wa Kidemokrasia ya Korea +KR,Republic of Korea,République de Corée,Republik Korea,República de Corea,جمهورية كوريا,大韩民国,Республика Корея,大韓民国,Jamhuri ya Korea +KW,Kuwait,Koweït,Kuwait,Kuwait,الكويت,科威特,Кувейт,クウェート,Kuwait +KY,Cayman Islands,Îles Caïmans,Kaimaninseln,Islas Caimán,جزر كايمان,开曼群岛,Каймановы острова,ケイマン諸島,Visiwa vya Cayman +KZ,Kazakhstan,Kazakhstan,Kasachstan,Kazajistán,كازاخستان,哈萨克斯坦,Казахстан,カザフスタン,Kazakistani +LA,Lao People's Democratic Republic,République démocratique populaire lao,Demokratische Volksrepublik Laos,República Democrática Popular Lao,جمهورية لاو الديمقراطية الشعبية,老挝人民民主共和国,Лаосская Народно-Демократическая Республика,ラオス人民民主共和国,Jamhuri ya Kidemokrasia ya Watu wa Lao +LB,Lebanon,Liban,Libanon,Líbano,لبنان,黎巴嫩,Ливан,レバノン,Lebanoni +LC,Saint Lucia,Sainte-Lucie,St. Lucia,Santa Lucía,سانت لوسيا,圣卢西亚,Сент-Люсия,セントルシア,Saint Lucia +LI,Liechtenstein,Liechtenstein,Liechtenstein,Liechtenstein,ليختنشتاين,列支敦士登,Лихтенштейн,リヒテンシュタイン,Liechtenstein +LK,Sri Lanka,Sri Lanka,Sri Lanka,Sri Lanka,سريلانكا,斯里兰卡,Шри-Ланка,スリランカ,Sri Lanka +LR,Liberia,Libéria,Liberia,Liberia,ليبيريا,利比里亚,Либерия,リベリア,Liberia +LS,Lesotho,Lesotho,Lesotho,Lesoto,ليسوتو,莱索托,Лесото,レソト,Lesotho +LT,Lithuania,Lituanie,Litauen,Lituania,ليتوانيا,立陶宛,Литва,リトアニア,Lithuania +LU,Luxembourg,Luxembourg,Luxemburg,Luxemburgo,لوكسمبورغ,卢森堡,Люксембург,ルクセンブルク,Luxemburg +LV,Latvia,Lettonie,Lettland,Letonia,لاتفيا,拉脱维亚,Латвия,ラトビア,Latvia +LY,Libya,Libye,Libyen,Libia,ليبيا,利比亚,Ливия,リビア,Libya +MA,Morocco,Maroc,Marokko,Marruecos,المغرب,摩洛哥,Марокко,モロッコ,Moroko +MC,Monaco,Monaco,Monaco,Mónaco,موناكو,摩纳哥,Монако,モナコ,Monako +MD,Moldova,Moldavie,Moldau,Moldavia,مولدوفا,摩尔多瓦,Молдова,モルドバ,Moldova +ME,Montenegro,Monténégro,Montenegro,Montenegro,الجبل الأسود,黑山,Черногория,モンテネグロ,Montenegro +MF,Saint Martin (French part),Saint-Martin (partie française),Saint-Martin (franz. Teil),San Martín (parte francesa),سان مارتن (الجزء الفرنسي),法属圣马丁,Сен-Мартен (фр. часть),サン・マルタン,Saint Martin (sehemu ya Ufaransa) +MG,Madagascar,Madagascar,Madagaskar,Madagascar,مدغشقر,马达加斯加,Мадагаскар,マダガスカル,Madagaska +MH,Marshall Islands,Îles Marshall,Marshallinseln,Islas Marshall,جزر مارشال,马绍尔群岛,Маршалловы Острова,マーシャル諸島,Visiwa vya Marshall +MK,North Macedonia,Macédoine du Nord,Nordmazedonien,Macedonia del Norte,مقدونيا الشمالية,北马其顿,Северная Македония,北マケドニア,Masedonia ya Kaskazini +ML,Mali,Mali,Mali,Malí,مالي,马里,Мали,マリ,Mali +MM,Myanmar,Myanmar,Myanmar,Myanmar,ميانمار,缅甸,Мьянма,ミャンマー,Myanmar +MN,Mongolia,Mongolie,Mongolei,Mongolia,منغوليا,蒙古,Монголия,モンゴル,Mongolia +MO,Macao,Macao,Macau,Macao,ماكاو,澳门,Макао,マカオ,Makao +MP,Northern Mariana Islands,Îles Mariannes du Nord,Nördliche Marianen,Islas Marianas del Norte,جزر ماريانا الشمالية,北马里亚纳群岛,Северные Марианские острова,北マリアナ諸島,Visiwa vya Mariana vya Kaskazini +MQ,Martinique,Martinique,Martinique,Martinica,مارتينيك,马提尼克,Мартиника,マルティニーク,Martiniki +MR,Mauritania,Mauritanie,Mauretanien,Mauritania,موريتانيا,毛里塔尼亚,Мавритания,モーリタニア,Moritania +MS,Montserrat,Montserrat,Montserrat,Montserrat,مونتسرات,蒙特塞拉特,Монтсеррат,モントセラト,Montserrat +MT,Malta,Malte,Malta,Malta,مالطا,马耳他,Мальта,マルタ,Malta +MU,Mauritius,Maurice,Mauritius,Mauricio,موريشيوس,毛里求斯,Маврикий,モーリシャス,Morisi +MV,Maldives,Maldives,Malediven,Maldivas,جزر المالديف,马尔代夫,Мальдивы,モルディブ,Maldivi +MW,Malawi,Malawi,Malawi,Malaui,ملاوي,马拉维,Малави,マラウイ,Malawi +MX,Mexico,Mexique,Mexiko,México,المكسيك,墨西哥,Мексика,メキシコ,Meksiko +MY,Malaysia,Malaisie,Malaysia,Malasia,ماليزيا,马来西亚,Малайзия,マレーシア,Malesia +MZ,Mozambique,Mozambique,Mosambik,Mozambique,موزمبيق,莫桑比克,Мозамбик,モザンビーク,Msumbiji +NA,Namibia,Namibie,Namibia,Namibia,ناميبيا,纳米比亚,Намибия,ナミビア,Namibia +NC,New Caledonia,Nouvelle-Calédonie,Neukaledonien,Nueva Caledonia,كاليدونيا الجديدة,新喀里多尼亚,Новая Каледония,ニューカレドニア,Nyukaledonia +NE,Niger,Niger,Niger,Níger,النيجر,尼日尔,Нигер,ニジェール,Nijeri +NF,Norfolk Island,Île Norfolk,Norfolkinsel,Isla Norfolk,جزيرة نورفولك,诺福克岛,Остров Норфолк,ノーフォーク島,Kisiwa cha Norfolk +NG,Nigeria,Nigéria,Nigeria,Nigeria,نيجيريا,尼日利亚,Нигерия,ナイジェリア,Nijeria +NI,Nicaragua,Nicaragua,Nicaragua,Nicaragua,نيكاراغوا,尼加拉瓜,Никарагуа,ニカラグア,Nikaragua +NL,Netherlands,Pays-Bas,Niederlande,Países Bajos,هولندا,荷兰,Нидерланды,オランダ,Uholanzi +NO,Norway,Norvège,Norwegen,Noruega,النرويج,挪威,Норвегия,ノルウェー,Norwe +NP,Nepal,Népal,Nepal,Nepal,نيبال,尼泊尔,Непал,ネパール,Nepali +NR,Nauru,Nauru,Nauru,Nauru,ناورو,瑙鲁,Науру,ナウル,Nauru +NT,Neutral Zone,Zone neutre,Neutrale Zone,Zona neutral,المنطقة المحايدة,中立区,Нейтральная зона,中立地帯,Eneo la Kutoegemea +NU,Niue,Niue,Niue,Niue,نييوي,纽埃,Ниуэ,ニウエ,Niue +NZ,New Zealand,Nouvelle-Zélande,Neuseeland,Nueva Zelanda,نيوزيلندا,新西兰,Новая Зеландия,ニュージーランド,Nyuzilandi +OM,Oman,Oman,Oman,Omán,عُمان,阿曼,Оман,オマーン,Omani +PA,Panama,Panama,Panama,Panamá,بنما,巴拿马,Панама,パナマ,Panama +PE,Peru,Pérou,Peru,Perú,بيرو,秘鲁,Перу,ペルー,Peru +PF,French Polynesia,Polynésie française,Französisch-Polynesien,Polinesia Francesa,بولينيزيا الفرنسية,法属波利尼西亚,Французская Полинезия,仏領ポリネシア,Polinesia ya Ufaransa +PG,Papua New Guinea,Papouasie-Nouvelle-Guinée,Papua-Neuguinea,Papúa Nueva Guinea,بابوا غينيا الجديدة,巴布亚新几内亚,Папуа — Новая Гвинея,パプアニューギニア,Papua Guinea Mpya +PH,Philippines,Philippines,Philippinen,Filipinas,الفلبين,菲律宾,Филиппины,フィリピン,Ufilipino +PK,Pakistan,Pakistan,Pakistan,Pakistán,باكستان,巴基斯坦,Пакистан,パキスタン,Pakistani +PL,Poland,Pologne,Polen,Polonia,بولندا,波兰,Польша,ポーランド,Polandi +PM,Saint Pierre and Miquelon,Saint-Pierre-et-Miquelon,Saint-Pierre und Miquelon,San Pedro y Miquelón,سان بيير وميكلون,圣皮埃尔和密克隆,Сен-Пьер и Микелон,サンピエール島・ミクロン島,Saint Pierre na Miquelon +PN,Pitcairn,Pitcairn,Pitcairninseln,Pitcairn,بيتكيرن,皮特凯恩群岛,Острова Питкэрн,ピトケアン諸島,Pitkairni +PR,Puerto Rico,Porto Rico,Puerto Rico,Puerto Rico,بورتوريكو,波多黎各,Пуэрто-Рико,プエルトリコ,Puerto Rico +PS,State of Palestine,État de Palestine,Staat Palästina,Estado de Palestina,دولة فلسطين,巴勒斯坦国,Государство Палестина,パレスチナ国,Nchi ya Palestina +PT,Portugal,Portugal,Portugal,Portugal,البرتغال,葡萄牙,Португалия,ポルトガル,Ureno +PW,Palau,Palaos,Palau,Palaos,بالاو,帕劳,Палау,パラオ,Palau +PY,Paraguay,Paraguay,Paraguay,Paraguay,باراغواي,巴拉圭,Парагвай,パラグアイ,Paragwai +QA,Qatar,Qatar,Katar,Catar,قطر,卡塔尔,Катар,カタール,Katari +QM..QZ,Private use,Usage privé,Privatnutzung,Uso privado,استخدام خاص,私人使用,Частное использование,私用,Matumizi binafsi +RE,Réunion,La Réunion,Réunion,Reunión,ريونيون,留尼汪,Реюньон,レユニオン,Reunion +RO,Romania,Roumanie,Rumänien,Rumanía,رومانيا,罗马尼亚,Румыния,ルーマニア,Romania +RS,Serbia,Serbie,Serbien,Serbia,صربيا,塞尔维亚,Сербия,セルビア,Serbia +RU,Russian Federation,Fédération de Russie,Russische Föderation,Federación de Rusia,الاتحاد الروسي,俄罗斯联邦,Российская Федерация,ロシア連邦,Shirikisho la Urusi +RW,Rwanda,Rwanda,Ruanda,Ruanda,رواندا,卢旺达,Руанда,ルワンダ,Rwanda +SA,Saudi Arabia,Arabie saoudite,Saudi-Arabien,Arabia Saudita,المملكة العربية السعودية,沙特阿拉伯,Саудовская Аравия,サウジアラビア,Saudia +SB,Solomon Islands,Îles Salomon,Salomonen,Islas Salomón,جزر سليمان,所罗门群岛,Соломоновы Острова,ソロモン諸島,Visiwa vya Solomon +SC,Seychelles,Seychelles,Seychellen,Seychelles,سيشل,塞舌尔,Сейшельские Острова,セーシェル,Shelisheli +SD,Sudan,Soudan,Sudan,Sudán,السودان,苏丹,Судан,スーダン,Sudani +SE,Sweden,Suède,Schweden,Suecia,السويد,瑞典,Швеция,スウェーデン,Uswidi +SG,Singapore,Singapour,Singapur,Singapur,سنغافورة,新加坡,Сингапур,シンガポール,Singapuri +"SH","Saint Helena, Ascension and Tristan da Cunha","Sainte-Hélène, Ascension et Tristan da Cunha","St. Helena, Ascension und Tristan da Cunha","Santa Elena, Ascensión y Tristán de Acuña",سانت هيلينا وأسنسيون وتريستان دا كونا,圣赫勒拿阿森松和特里斯坦-达库尼亚,"Острова Святой Елены, Вознесения и Тристан-да-Кунья",セントヘレナ・アセンションおよびトリスタンダクーニャ,Saint Helena Ascension na Tristan da Cunha +SI,Slovenia,Slovénie,Slowenien,Eslovenia,سلوفينيا,斯洛文尼亚,Словения,スロベニア,Slovenia +SJ,Svalbard and Jan Mayen,Svalbard et Jan Mayen,Svalbard und Jan Mayen,Svalbard y Jan Mayen,سفالبارد ويان ماين,斯瓦尔巴和扬马延,Шпицберген и Ян-Майен,スヴァールバル諸島・ヤンマイエン島,Svalbard na Jan Mayen +SK,Slovakia,Slovaquie,Slowakei,Eslovaquia,سلوفاكيا,斯洛伐克,Словакия,スロバキア,Slovakia +SL,Sierra Leone,Sierra Leone,Sierra Leone,Sierra Leona,سيراليون,塞拉利昂,Сьерра-Леоне,シエラレオネ,Siera Leoni +SM,San Marino,Saint-Marin,San Marino,San Marino,سان مارينو,圣马力诺,Сан-Марино,サンマリノ,San Marino +SN,Senegal,Sénégal,Senegal,Senegal,السنغال,塞内加尔,Сенегал,セネガル,Senegali +SO,Somalia,Somalie,Somalia,Somalia,الصومال,索马里,Сомали,ソマリア,Somalia +SR,Suriname,Suriname,Suriname,Surinam,سورينام,苏里南,Суринам,スリナム,Surinamu +SS,South Sudan,Soudan du Sud,Südsudan,Sudán del Sur,جنوب السودان,南苏丹,Южный Судан,南スーダン,Sudani Kusini +ST,Sao Tome and Principe,Sao Tomé-et-Príncipe,São Tomé und Príncipe,Santo Tomé y Príncipe,ساو تومي وبرينسيبي,圣多美和普林西比,Сан-Томе и Принсипи,サントメ・プリンシペ,Sao Tome na Principe +SU,Union of Soviet Socialist Republics,Union des républiques socialistes soviétiques,Union der Sozialistischen Sowjetrepubliken,Unión de Repúblicas Socialistas Soviéticas,اتحاد الجمهوريات الاشتراكية السوفيتية,苏维埃社会主义共和国联盟,Союз Советских Социалистических Республик,ソビエト社会主義共和国連邦,Muungano wa Jamhuri za Kisoshalisti za Sovieti +SV,El Salvador,El Salvador,El Salvador,El Salvador,السلفادور,萨尔瓦多,Сальвадор,エルサルバドル,El Salvador +SX,Sint Maarten (Dutch part),Saint-Martin (partie néerlandaise),Sint Maarten (niederl. Teil),San Martín (parte neerlandesa),سينت مارتن (الجزء الهولندي),荷属圣马丁,Синт-Мартен (нидерл. часть),シント・マールテン,Sint Maarten (sehemu ya Kiholanzi) +SY,Syrian Arab Republic,République arabe syrienne,Arabische Republik Syrien,República Árabe Siria,الجمهورية العربية السورية,阿拉伯叙利亚共和国,Сирийская Арабская Республика,シリア・アラブ共和国,Jamhuri ya Kiarabu ya Syria +SZ,Eswatini,Eswatini,Eswatini,Esuatini,إسواتيني,斯威士兰,Эсватини,エスワティニ,Eswatini +TA,Tristan da Cunha,Tristan da Cunha,Tristan da Cunha,Tristán de Acuña,تريستان دا كونا,特里斯坦-达库尼亚,Тристан-да-Кунья,トリスタンダクーニャ,Tristan da Cunha +TC,Turks and Caicos Islands,Îles Turques-et-Caïques,Turks- und Caicosinseln,Islas Turcas y Caicos,جزر تركس وكايكوس,特克斯和凯科斯群岛,Тёркс и Кайкос,タークス・カイコス諸島,Visiwa vya Turks na Caicos +TD,Chad,Tchad,Tschad,Chad,تشاد,乍得,Чад,チャド,Chadi +TF,French Southern Territories,Terres australes françaises,Französische Südgebiete,Tierras Australes Francesas,الأراضي الفرنسية الجنوبية,法属南部领地,Французские Южные Территории,仏領南方地域,Maeneo ya Kusini ya Ufaransa +TG,Togo,Togo,Togo,Togo,توغو,多哥,Того,トーゴ,Togo +TH,Thailand,Thaïlande,Thailand,Tailandia,تايلاند,泰国,Таиланд,タイ,Thailandi +TJ,Tajikistan,Tadjikistan,Tadschikistan,Tayikistán,طاجيكستان,塔吉克斯坦,Таджикистан,タジキスタン,Tajikistani +TK,Tokelau,Tokelau,Tokelau,Tokelau,توكيلاو,托克劳,Токелау,トケラウ,Tokelau +TL,Timor-Leste,Timor-Leste,Timor-Leste,Timor-Leste,تيمور الشرقية,东帝汶,Тимор-Лешти,東ティモール,Timor-Leste +TM,Turkmenistan,Turkménistan,Turkmenistan,Turkmenistán,تركمانستان,土库曼斯坦,Туркменистан,トルクメニスタン,Turukimenistani +TN,Tunisia,Tunisie,Tunesien,Túnez,تونس,突尼斯,Тунис,チュニジア,Tunisia +TO,Tonga,Tonga,Tonga,Tonga,تونغا,汤加,Тонга,トンガ,Tonga +TP,East Timor,Timor oriental,Osttimor,Timor Oriental,تيمور الشرقية,东帝汶,Восточный Тимор,東ティモール,Timor ya Mashariki +TR,Türkiye,Türkiye,Türkiye,Türkiye,تركيا,土耳其,Турция,トルコ,Uturuki +TT,Trinidad and Tobago,Trinité-et-Tobago,Trinidad und Tobago,Trinidad y Tobago,ترينيداد وتوباغو,特立尼达和多巴哥,Тринидад и Тобаго,トリニダード・トバゴ,Trinidad na Tobago +TV,Tuvalu,Tuvalu,Tuvalu,Tuvalu,توفالو,图瓦卢,Тувалу,ツバル,Tuvalu +"TW","Taiwan, Province of China","Taïwan, province de Chine","Taiwan, Provinz Chinas","Taiwán, provincia de China",تايوان,中国台湾省,"Тайвань, провинция Китая",台湾,Taiwan +TZ,United Republic of Tanzania,République-Unie de Tanzanie,Vereinigte Republik Tansania,República Unida de Tanzania,جمهورية تنزانيا المتحدة,坦桑尼亚联合共和国,Объединённая Республика Танзания,タンザニア連合共和国,Jamhuri ya Muungano wa Tanzania +UA,Ukraine,Ukraine,Ukraine,Ucrania,أوكرانيا,乌克兰,Украина,ウクライナ,Ukraine +UG,Uganda,Ouganda,Uganda,Uganda,أوغندا,乌干达,Уганда,ウガンダ,Uganda +UM,United States Minor Outlying Islands,Îles mineures éloignées des États-Unis,Kleinere Amerikanische Überseeinseln,Islas Ultramarinas Menores de Estados Unidos,جزر الولايات المتحدة الصغيرة النائية,美国本土外小岛屿,Малые Тихоокеанские Отдалённые Острова США,合衆国領有小離島,Visiwa Vidogo vya Nje vya Marekani +UN,United Nations,Nations Unies,Vereinte Nationen,Naciones Unidas,الأمم المتحدة,联合国,Организация Объединённых Наций,国際連合,Umoja wa Mataifa +US,United States,États-Unis,Vereinigte Staaten,Estados Unidos,الولايات المتحدة,美国,Соединённые Штаты,アメリカ合衆国,Marekani +UY,Uruguay,Uruguay,Uruguay,Uruguay,أوروغواي,乌拉圭,Уругвай,ウルグアイ,Urugwai +UZ,Uzbekistan,Ouzbékistan,Usbekistan,Uzbekistán,أوزبكستان,乌兹别克斯坦,Узбекистан,ウズベキスタン,Uzibekistani +VA,Holy See (Vatican City State),Saint-Siège (État de la Cité du Vatican),Heiliger Stuhl (Staat Vatikanstadt),Santa Sede (Estado de la Ciudad del Vaticano),الكرسي الرسولي (دولة مدينة الفاتيكان),梵蒂冈,Святой Престол (Государство Ватикан),バチカン市国,Vatican +VC,Saint Vincent and the Grenadines,Saint-Vincent-et-les-Grenadines,St. Vincent und die Grenadinen,San Vicente y las Granadinas,سانت فنسنت وجزر غرينادين,圣文森特和格林纳丁斯,Сент-Винсент и Гренадины,セントビンセント・グレナディーン,Saint Vincent na Grenadini +VE,Venezuela,Venezuela,Venezuela,Venezuela,فنزويلا,委内瑞拉,Венесуэла,ベネズエラ,Venezuela +VG,British Virgin Islands,Îles Vierges britanniques,Britische Jungferninseln,Islas Vírgenes Británicas,جزر العذراء البريطانية,英属维尔京群岛,Британские Виргинские острова,英領ヴァージン諸島,Visiwa vya Virgin vya Uingereza +VI,U.S. Virgin Islands,Îles Vierges américaines,Amerikanische Jungferninseln,Islas Vírgenes de los Estados Unidos,جزر العذراء الأمريكية,美属维尔京群岛,Виргинские острова США,米領ヴァージン諸島,Visiwa vya Virgin vya Marekani +VN,Viet Nam,Viêt Nam,Vietnam,Vietnam,فيتنام,越南,Вьетнам,ベトナム,Vietnamu +VU,Vanuatu,Vanuatu,Vanuatu,Vanuatu,فانواتو,瓦努阿图,Вануату,バヌアツ,Vanuatu +WF,Wallis and Futuna,Wallis-et-Futuna,Wallis und Futuna,Wallis y Futuna,واليس وفوتونا,瓦利斯和富图纳,Уоллис и Футуна,ウォリス・フツナ,Wallis na Futuna +WS,Samoa,Samoa,Samoa,Samoa,ساموا,萨摩亚,Самоа,サモア,Samoa +YD,Democratic Yemen,Yémen démocratique,Demokratischer Jemen,Yemen Democrático,اليمن الديمقراطي,民主也门,Демократический Йемен,民主イエメン,Yemen ya Kidemokrasia +YE,Yemen,Yémen,Jemen,Yemen,اليمن,也门,Йемен,イエメン,Yemen +YT,Mayotte,Mayotte,Mayotte,Mayotte,مايوت,马约特,Майотта,マヨット,Mayotte +YU,Yugoslavia,Yougoslavie,Jugoslawien,Yugoslavia,يوغوسلافيا,南斯拉夫,Югославия,ユーゴスラビア,Yugoslavia +ZA,South Africa,Afrique du Sud,Südafrika,Sudáfrica,جنوب أفريقيا,南非,Южная Африка,南アフリカ,Afrika Kusini +ZM,Zambia,Zambie,Sambia,Zambia,زامبيا,赞比亚,Замбия,ザンビア,Zambia +ZR,Zaire,Zaïre,Zaire,Zaire,زائير,扎伊尔,Заир,ザイール,Zaire +ZW,Zimbabwe,Zimbabwe,Simbabwe,Zimbabue,زيمبابوي,津巴布韦,Зимбабве,ジンバブエ,Zimbabwe \ No newline at end of file diff --git a/tx/library.js b/tx/library.js index e6328a0..fb2cbdc 100644 --- a/tx/library.js +++ b/tx/library.js @@ -140,7 +140,7 @@ class Library { async load() { this.startTime = Date.now(); - this.languageDefinitions = await LanguageDefinitions.fromFile(path.join(__dirname, '../tx/data/lang.dat')); + this.languageDefinitions = await LanguageDefinitions.fromFiles(path.join(__dirname, '../tx/data')); this.i18n = new I18nSupport(path.join(__dirname, '../translations'), this.languageDefinitions); await this.i18n.load(); diff --git a/tx/library/bundle.js b/tx/library/bundle.js new file mode 100644 index 0000000..e9c4519 --- /dev/null +++ b/tx/library/bundle.js @@ -0,0 +1,5 @@ + +class Bundle { +} + +module.exports = { Bundle }; \ No newline at end of file diff --git a/tx/library/capabilitystatement.js b/tx/library/capabilitystatement.js index b98938a..3fb1f5e 100644 --- a/tx/library/capabilitystatement.js +++ b/tx/library/capabilitystatement.js @@ -1,5 +1,5 @@ const {CanonicalResource} = require("./canonical-resource"); -const {VersionUtilities} = require("../../library/version-utilities"); +const {capabilityStatementFromR5, capabilityStatementToR5} = require("../xversion/xv-capabiliityStatement"); /** * Represents a FHIR CapabilityStatement resource with version conversion support @@ -15,7 +15,7 @@ class CapabilityStatement extends CanonicalResource { constructor(jsonObj, fhirVersion = 'R5') { super(jsonObj, fhirVersion); // Convert to R5 format internally (modifies input for performance) - this.jsonObj = this._convertToR5(jsonObj, fhirVersion); + this.jsonObj = capabilityStatementToR5(jsonObj, fhirVersion); this.validate(); this.id = this.jsonObj.id; } @@ -46,146 +46,7 @@ class CapabilityStatement extends CanonicalResource { * @returns {Object} JSON object */ toJSON(version = 'R5') { - return this._convertFromR5(this.jsonObj, version); - } - - /** - * Converts input CapabilityStatement to R5 format (modifies input object for performance) - * @param {Object} jsonObj - The input CapabilityStatement object - * @param {string} version - Source FHIR version - * @returns {Object} The same object, potentially modified to R5 format - * @private - */ - _convertToR5(jsonObj, version) { - if (version === 'R5') { - return jsonObj; // Already R5, no conversion needed - } - - if (version === 'R3') { - // R3: resourceType was "CapabilityStatement" (same as R4/R5) - // Convert identifier from single object to array if present - if (jsonObj.identifier && !Array.isArray(jsonObj.identifier)) { - jsonObj.identifier = [jsonObj.identifier]; - } - return jsonObj; - } - - if (version === 'R4') { - // R4 to R5: No major structural changes needed - return jsonObj; - } - - throw new Error(`Unsupported FHIR version: ${version}`); - } - - /** - * Converts R5 CapabilityStatement to target version format (clones object first) - * @param {Object} r5Obj - The R5 format CapabilityStatement object - * @param {string} targetVersion - Target FHIR version - * @returns {Object} New object in target version format - * @private - */ - _convertFromR5(r5Obj, targetVersion) { - if (VersionUtilities.isR5Ver(targetVersion)) { - return r5Obj; // No conversion needed - } - - // Clone the object to avoid modifying the original - const cloned = JSON.parse(JSON.stringify(r5Obj)); - - if (VersionUtilities.isR4Ver(targetVersion)) { - return this._convertR5ToR4(cloned); - } else if (VersionUtilities.isR3Ver(targetVersion)) { - return this._convertR5ToR3(cloned); - } - - throw new Error(`Unsupported target FHIR version: ${targetVersion}`); - } - - /** - * Converts R5 CapabilityStatement to R4 format - * @param {Object} r5Obj - Cloned R5 CapabilityStatement object - * @returns {Object} R4 format CapabilityStatement - * @private - */ - _convertR5ToR4(r5Obj) { - // Remove R5-specific elements - if (r5Obj.versionAlgorithmString) { - delete r5Obj.versionAlgorithmString; - } - if (r5Obj.versionAlgorithmCoding) { - delete r5Obj.versionAlgorithmCoding; - } - - return r5Obj; - } - - /** - * Converts R5 CapabilityStatement to R3 format - * @param {Object} r5Obj - Cloned R5 CapabilityStatement object - * @returns {Object} R3 format CapabilityStatement - * @private - */ - _convertR5ToR3(r5Obj) { - // First apply R4 conversions - const r4Obj = this._convertR5ToR4(r5Obj); - - // Convert identifier array back to single object - if (r4Obj.identifier && Array.isArray(r4Obj.identifier)) { - if (r4Obj.identifier.length > 0) { - r4Obj.identifier = r4Obj.identifier[0]; - } else { - delete r4Obj.identifier; - } - } - - // Convert valueCanonical to valueUri throughout the object - this._convertCanonicalToUri(r5Obj); - - - // Convert rest.operation.definition from canonical string to Reference object - for (const rest of r4Obj.rest || []) { - for (const operation of rest.operation || []) { - if (typeof operation.definition === 'string') { - operation.definition = {reference: operation.definition}; - } - for (const resource of rest.resource || []) { - delete resource.operation; - } - } - } - - return r4Obj; - } - - /** - * Recursively converts valueCanonical to valueUri in an object - * R3 doesn't have canonical type, so valueCanonical must become valueUri - * @param {Object} obj - Object to convert - * @private - */ - _convertCanonicalToUri(obj) { - if (!obj || typeof obj !== 'object') { - return; - } - - if (Array.isArray(obj)) { - obj.forEach(item => this._convertCanonicalToUri(item)); - return; - } - - // Convert valueCanonical to valueUri - if (obj.valueCanonical !== undefined) { - obj.valueUri = obj.valueCanonical; - delete obj.valueCanonical; - } - - // Recurse into all properties - for (const key of Object.keys(obj)) { - if (typeof obj[key] === 'object') { - this._convertCanonicalToUri(obj[key]); - } - } + return capabilityStatementFromR5(this.jsonObj, version); } /** diff --git a/tx/library/codesystem.js b/tx/library/codesystem.js index 6605c70..a1a8a99 100644 --- a/tx/library/codesystem.js +++ b/tx/library/codesystem.js @@ -1,6 +1,7 @@ const { Language } = require("../../library/languages"); const {CanonicalResource} = require("./canonical-resource"); -const {VersionUtilities} = require("../../library/version-utilities"); +const {codeSystemFromR5, codeSystemToR5} = require("../xversion/xv-codesystem"); +const {getValuePrimitive} = require("../../library/utilities"); const CodeSystemContentMode = Object.freeze({ Complete: 'complete', @@ -27,12 +28,14 @@ class CodeSystem extends CanonicalResource { * @param {string} jsonObj.status - Publication status (draft|active|retired|unknown) * @param {Object[]} [jsonObj.concept] - Array of concept definitions */ - constructor(jsonObj, fhirVersion = 'R5') { + constructor(jsonObj, fhirVersion = 'R5', noMaps = false) { super(jsonObj, fhirVersion); // Convert to R5 format internally (modifies input for performance) - this.jsonObj = this._convertToR5(this.jsonObj, fhirVersion); - this.validate(); - this.buildMaps(); + this.jsonObj = codeSystemToR5(this.jsonObj, fhirVersion); + if (!noMaps) { + this.validate(); + this.buildMaps(); + } } /** @@ -81,175 +84,10 @@ class CodeSystem extends CanonicalResource { * @returns {string} JSON string */ toJSONString(version = 'R5') { - const outputObj = this._convertFromR5(this.jsonObj, version); + const outputObj = codeSystemFromR5(this.jsonObj, version); return JSON.stringify(outputObj); } - /** - * Converts input CodeSystem to R5 format (modifies input object for performance) - * @param {Object} jsonObj - The input CodeSystem object - * @param {string} version - Source FHIR version - * @returns {Object} The same object, potentially modified to R5 format - * @private - */ - _convertToR5(jsonObj, version) { - if (version === 'R5') { - return jsonObj; // Already R5, no conversion needed - } - - if (version === 'R3') { - // R3 to R5: Convert identifier from single object to array - if (jsonObj.identifier && !Array.isArray(jsonObj.identifier)) { - jsonObj.identifier = [jsonObj.identifier]; - } - return jsonObj; - } - - if (version === 'R4') { - // R4 to R5: identifier is already an array, no conversion needed - return jsonObj; - } - - throw new Error(`Unsupported FHIR version: ${version}`); - } - - /** - * Converts R5 CodeSystem to target version format (clones object first) - * @param {Object} r5Obj - The R5 format CodeSystem object - * @param {string} targetVersion - Target FHIR version - * @returns {Object} New object in target version format - * @private - */ - _convertFromR5(r5Obj, targetVersion) { - if (VersionUtilities.isR5Ver(targetVersion)) { - return r5Obj; // No conversion needed - } - - // Clone the object to avoid modifying the original - const cloned = JSON.parse(JSON.stringify(r5Obj)); - - if (VersionUtilities.isR4Ver(targetVersion)) { - return this._convertR5ToR4(cloned); - } else if (VersionUtilities.isR3Ver(targetVersion)) { - return this._convertR5ToR3(cloned); - } - - throw new Error(`Unsupported target FHIR version: ${targetVersion}`); - } - - /** - * Converts R5 CodeSystem to R4 format - * @param {Object} r5Obj - Cloned R5 CodeSystem object - * @returns {Object} R4 format CodeSystem - * @private - */ - _convertR5ToR4(r5Obj) { - // Remove R5-specific elements that don't exist in R4 - if (r5Obj.versionAlgorithmString) { - delete r5Obj.versionAlgorithmString; - } - if (r5Obj.versionAlgorithmCoding) { - delete r5Obj.versionAlgorithmCoding; - } - - // Filter out R5-only filter operators - if (r5Obj.filter && Array.isArray(r5Obj.filter)) { - r5Obj.filter = r5Obj.filter.map(filter => { - if (filter.operator && Array.isArray(filter.operator)) { - // Remove R5-only operators like 'generalizes' - filter.operator = filter.operator.filter(op => - !this._isR5OnlyFilterOperator(op) - ); - } - return filter; - }).filter(filter => - // Remove filters that have no valid operators left - !filter.operator || filter.operator.length > 0 - ); - } - - return r5Obj; - } - - /** - * Converts R5 CodeSystem to R3 format - * @param {Object} r5Obj - Cloned R5 CodeSystem object - * @returns {Object} R3 format CodeSystem - * @private - */ - _convertR5ToR3(r5Obj) { - // First apply R4 conversions - const r4Obj = this._convertR5ToR4(r5Obj); - - // R5/R4 to R3: Convert identifier from array back to single object - if (r4Obj.identifier && Array.isArray(r4Obj.identifier)) { - if (r4Obj.identifier.length > 0) { - // Take the first identifier if multiple exist - r4Obj.identifier = r4Obj.identifier[0]; - } else { - // Remove empty array - delete r4Obj.identifier; - } - } - - // Remove additional R4-specific elements that don't exist in R3 - if (r4Obj.supplements) { - delete r4Obj.supplements; - } - - // R3 has more limited filter operator support - if (r4Obj.filter && Array.isArray(r4Obj.filter)) { - r4Obj.filter = r4Obj.filter.map(filter => { - if (filter.operator && Array.isArray(filter.operator)) { - // Keep only R3-compatible operators - filter.operator = filter.operator.filter(op => - this._isR3CompatibleFilterOperator(op) - ); - } - return filter; - }).filter(filter => - // Remove filters that have no valid operators left - !filter.operator || filter.operator.length > 0 - ); - } - - return r4Obj; - } - - /** - * Checks if a filter operator is R5-only - * @param {string} operator - Filter operator code - * @returns {boolean} True if operator is R5-only - * @private - */ - _isR5OnlyFilterOperator(operator) { - const r5OnlyOperators = [ - 'generalizes', // Added in R5 - // Add other R5-only operators as they're identified - ]; - return r5OnlyOperators.includes(operator); - } - - /** - * Checks if a filter operator is compatible with R3 - * @param {string} operator - Filter operator code - * @returns {boolean} True if operator is R3-compatible - * @private - */ - _isR3CompatibleFilterOperator(operator) { - const r3CompatibleOperators = [ - '=', // Equal - 'is-a', // Is-A relationship - 'descendent-of', // Descendant of (note: R3 spelling) - 'is-not-a', // Is-Not-A relationship - 'regex', // Regular expression - 'in', // In set - 'not-in', // Not in set - 'exists', // Property exists - ]; - return r3CompatibleOperators.includes(operator); - } - /** * Validates that this is a proper CodeSystem resource * @throws {Error} If validation fails @@ -769,6 +607,10 @@ class CodeSystem extends CanonicalResource { set id(value) { this.jsonObj.id = value; } + + isLangPack() { + return (this.jsonObj.extension || []).find(x => x.url == 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-type' && getValuePrimitive(x) == 'lang-pack'); + } } module.exports = { CodeSystem, CodeSystemContentMode }; \ No newline at end of file diff --git a/tx/library/conceptmap.js b/tx/library/conceptmap.js index ee47dd8..00c2578 100644 --- a/tx/library/conceptmap.js +++ b/tx/library/conceptmap.js @@ -1,5 +1,6 @@ const {CanonicalResource} = require("./canonical-resource"); const {VersionUtilities} = require("../../library/version-utilities"); +const {conceptMapToR5, conceptMapFromR5} = require("../xversion/xv-conceptmap"); /** * Represents a FHIR ConceptMap resource with version conversion support @@ -16,7 +17,7 @@ class ConceptMap extends CanonicalResource { constructor(jsonObj, fhirVersion = 'R5') { super(jsonObj, fhirVersion); // Convert to R5 format internally (modifies input for performance) - this.jsonObj = this._convertToR5(jsonObj, fhirVersion); + this.jsonObj = conceptMapToR5(jsonObj, fhirVersion); this.validate(); this.id = this.jsonObj.id; } @@ -37,226 +38,11 @@ class ConceptMap extends CanonicalResource { * @returns {string} JSON string */ toJSONString(version = 'R5') { - const outputObj = this._convertFromR5(this.jsonObj, version); + const outputObj = conceptMapFromR5(this.jsonObj, version); return JSON.stringify(outputObj); } - /** - * Converts input ConceptMap to R5 format (modifies input object for performance) - * @param {Object} jsonObj - The input ConceptMap object - * @param {string} version - Source FHIR version - * @returns {Object} The same object, potentially modified to R5 format - * @private - */ - _convertToR5(jsonObj, version) { - if (VersionUtilities.isR5Ver(version)) { - return jsonObj; // Already R5, no conversion needed - } - - if (VersionUtilities.isR3Ver(version) || VersionUtilities.isR4Ver(version)) { - // Convert identifier from single object to array - if (jsonObj.identifier && !Array.isArray(jsonObj.identifier)) { - jsonObj.identifier = [jsonObj.identifier]; - } - - // Convert source/target to sourceScope/targetScope - if (jsonObj.source !== undefined) { - // Combine source + sourceVersion if both exist - if (jsonObj.sourceVersion) { - jsonObj.sourceScope = `${jsonObj.source}|${jsonObj.sourceVersion}`; - delete jsonObj.sourceVersion; - } else { - jsonObj.sourceScope = jsonObj.source; - } - delete jsonObj.source; - } - - if (jsonObj.target !== undefined) { - // Combine target + targetVersion if both exist - if (jsonObj.targetVersion) { - jsonObj.targetScope = `${jsonObj.target}|${jsonObj.targetVersion}`; - delete jsonObj.targetVersion; - } else { - jsonObj.targetScope = jsonObj.target; - } - delete jsonObj.target; - } - - // Convert equivalence to relationship in group.element.target - if (jsonObj.group && Array.isArray(jsonObj.group)) { - jsonObj.group.forEach(group => { - if (group.element && Array.isArray(group.element)) { - group.element.forEach(element => { - if (element.target && Array.isArray(element.target)) { - element.target.forEach(target => { - if (target.equivalence && !target.relationship) { - // Convert equivalence to relationship and keep both - target.relationship = this._convertEquivalenceToRelationship(target.equivalence); - // Keep equivalence for backward compatibility - } - }); - } - }); - } - }); - } - - return jsonObj; - } - - throw new Error(`Unsupported FHIR version: ${version}`); - } - - /** - * Converts R5 ConceptMap to target version format (clones object first) - * @param {Object} r5Obj - The R5 format ConceptMap object - * @param {string} targetVersion - Target FHIR version - * @returns {Object} New object in target version format - * @private - */ - _convertFromR5(r5Obj, targetVersion) { - if (VersionUtilities.isR5Ver(targetVersion)) { - return r5Obj; // No conversion needed - } - - // Clone the object to avoid modifying the original - const cloned = JSON.parse(JSON.stringify(r5Obj)); - - if (VersionUtilities.isR4Ver(targetVersion)) { - return this._convertR5ToR4(cloned); - } else if (VersionUtilities.isR3Ver(targetVersion)) { - return this._convertR5ToR3(cloned); - } - - throw new Error(`Unsupported target FHIR version: ${targetVersion}`); - } - - /** - * Converts R5 ConceptMap to R4 format - * @param {Object} r5Obj - Cloned R5 ConceptMap object - * @returns {Object} R4 format ConceptMap - * @private - */ - _convertR5ToR4(r5Obj) { - // Remove R5-specific elements - if (r5Obj.versionAlgorithmString) { - delete r5Obj.versionAlgorithmString; - } - if (r5Obj.versionAlgorithmCoding) { - delete r5Obj.versionAlgorithmCoding; - } - if (r5Obj.property) { - delete r5Obj.property; - } - if (r5Obj.additionalAttribute) { - delete r5Obj.additionalAttribute; - } - - // Convert identifier array back to single object - if (r5Obj.identifier && Array.isArray(r5Obj.identifier)) { - if (r5Obj.identifier.length > 0) { - r5Obj.identifier = r5Obj.identifier[0]; // Take first identifier - } else { - delete r5Obj.identifier; - } - } - - // Convert sourceScope/targetScope back to source/target + version - if (r5Obj.sourceScope) { - const parts = r5Obj.sourceScope.split('|'); - r5Obj.source = parts[0]; - if (parts.length > 1) { - r5Obj.sourceVersion = parts[1]; - } - delete r5Obj.sourceScope; - } - - if (r5Obj.targetScope) { - const parts = r5Obj.targetScope.split('|'); - r5Obj.target = parts[0]; - if (parts.length > 1) { - r5Obj.targetVersion = parts[1]; - } - delete r5Obj.targetScope; - } - - // Convert relationship back to equivalence in group.element.target - if (r5Obj.group && Array.isArray(r5Obj.group)) { - r5Obj.group.forEach(group => { - if (group.element && Array.isArray(group.element)) { - group.element.forEach(element => { - if (element.target && Array.isArray(element.target)) { - element.target.forEach(target => { - // If we have both equivalence and relationship, prefer equivalence for R4 - if (target.relationship && !target.equivalence) { - target.equivalence = this._convertRelationshipToEquivalence(target.relationship); - } - // Remove R5-only relationship field - delete target.relationship; - }); - } - }); - } - }); - } - - return r5Obj; - } - - /** - * Converts R5 ConceptMap to R3 format - * @param {Object} r5Obj - Cloned R5 ConceptMap object - * @returns {Object} R3 format ConceptMap - * @private - */ - _convertR5ToR3(r5Obj) { - // First apply R4 conversions - const r4Obj = this._convertR5ToR4(r5Obj); - - // R3 has the same structure as R4 for the elements we care about - return r4Obj; - } - - /** - * Converts R3/R4 equivalence to R5 relationship - * @param {string} equivalence - R3/R4 equivalence value - * @returns {string} R5 relationship value - * @private - */ - _convertEquivalenceToRelationship(equivalence) { - const equivalenceToRelationship = { - 'relatedto': 'related-to', - 'equivalent': 'equivalent', - 'equal': 'equivalent', - 'wider': 'source-is-broader-than-target', - 'subsumes': 'source-is-broader-than-target', - 'narrower': 'source-is-narrower-than-target', - 'specializes': 'source-is-narrower-than-target', - 'inexact': 'not-related-to', - 'unmatched': 'not-related-to', - 'disjoint': 'not-related-to' - }; - return equivalenceToRelationship[equivalence] || 'related-to'; - } - - /** - * Converts R5 relationship back to R3/R4 equivalence - * @param {string} relationship - R5 relationship value - * @returns {string} R3/R4 equivalence value - * @private - */ - _convertRelationshipToEquivalence(relationship) { - const relationshipToEquivalence = { - 'related-to': 'relatedto', - 'equivalent': 'equivalent', - 'source-is-broader-than-target': 'wider', - 'source-is-narrower-than-target': 'narrower', - 'not-related-to': 'unmatched' - }; - return relationshipToEquivalence[relationship] || 'relatedto'; - } - - /** + /** * Gets the FHIR version this ConceptMap was loaded from * @returns {string} FHIR version ('R3', 'R4', or 'R5') */ diff --git a/tx/library/namingsystem.js b/tx/library/namingsystem.js index 8a1961a..aa4f7e3 100644 --- a/tx/library/namingsystem.js +++ b/tx/library/namingsystem.js @@ -1,4 +1,4 @@ -const {VersionUtilities} = require("../../library/version-utilities"); +const {namingSystemToR5, namingSystemFromR5} = require("../xversion/xv-namingsystem"); /** * Represents a FHIR NamingSystem resource with version conversion support @@ -40,7 +40,7 @@ class NamingSystem { constructor(jsonObj, version = 'R5') { this.version = version; // Convert to R5 format internally (modifies input for performance) - this.jsonObj = this._convertToR5(jsonObj, version); + this.jsonObj = namingSystemToR5(jsonObj, version); this.validate(); } @@ -50,96 +50,10 @@ class NamingSystem { * @returns {string} JSON string */ toJSONString(version = 'R5') { - const outputObj = this._convertFromR5(this.jsonObj, version); + const outputObj = namingSystemFromR5(this.jsonObj, version); return JSON.stringify(outputObj); } - /** - * Converts input NamingSystem to R5 format (modifies input object for performance) - * @param {Object} jsonObj - The input NamingSystem object - * @param {string} version - Source FHIR version - * @returns {Object} The same object, potentially modified to R5 format - * @private - */ - _convertToR5(jsonObj, version) { - if (version === 'R5') { - return jsonObj; // Already R5, no conversion needed - } - - if (version === 'R3') { - // R3 to R5: Remove replacedBy field (we ignore it completely) - if (jsonObj.replacedBy !== undefined) { - delete jsonObj.replacedBy; - } - return jsonObj; - } - - if (version === 'R4') { - // R4 to R5: No structural conversion needed - // R5 is backward compatible for the structural elements we care about - return jsonObj; - } - - throw new Error(`Unsupported FHIR version: ${version}`); - } - - /** - * Converts R5 NamingSystem to target version format (clones object first) - * @param {Object} r5Obj - The R5 format NamingSystem object - * @param {string} targetVersion - Target FHIR version - * @returns {Object} New object in target version format - * @private - */ - _convertFromR5(r5Obj, targetVersion) { - if (VersionUtilities.isR5Ver(targetVersion)) { - return r5Obj; // No conversion needed - } - - // Clone the object to avoid modifying the original - const cloned = JSON.parse(JSON.stringify(r5Obj)); - - if (VersionUtilities.isR4Ver(targetVersion)) { - return this._convertR5ToR4(cloned); - } else if (VersionUtilities.isR3Ver(targetVersion)) { - return this._convertR5ToR3(cloned); - } - - throw new Error(`Unsupported target FHIR version: ${targetVersion}`); - } - - /** - * Converts R5 NamingSystem to R4 format - * @param {Object} r5Obj - Cloned R5 NamingSystem object - * @returns {Object} R4 format NamingSystem - * @private - */ - _convertR5ToR4(r5Obj) { - // Remove R5-specific elements that don't exist in R4 - if (r5Obj.versionAlgorithmString) { - delete r5Obj.versionAlgorithmString; - } - if (r5Obj.versionAlgorithmCoding) { - delete r5Obj.versionAlgorithmCoding; - } - - return r5Obj; - } - - /** - * Converts R5 NamingSystem to R3 format - * @param {Object} r5Obj - Cloned R5 NamingSystem object - * @returns {Object} R3 format NamingSystem - * @private - */ - _convertR5ToR3(r5Obj) { - // First apply R4 conversions - const r4Obj = this._convertR5ToR4(r5Obj); - - // R3 doesn't have some R4/R5 fields, but we'll just let them through - // since most additions are backward compatible in JSON - - return r4Obj; - } /** * Gets the FHIR version this NamingSystem was loaded from diff --git a/tx/library/parameters.js b/tx/library/parameters.js index 9a0ff76..a3d7197 100644 --- a/tx/library/parameters.js +++ b/tx/library/parameters.js @@ -1,10 +1,11 @@ const {getValuePrimitive, getValueDT} = require("../../library/utilities"); +const {parametersToR5} = require("../xversion/xv-parameters"); class Parameters { jsonObj; - constructor (jsonObj = null) { - this.jsonObj = jsonObj ? jsonObj : { "resourceType": "Parameters" }; + constructor (jsonObj = null, fhirVersion = 'R5') { + this.jsonObj = parametersToR5(jsonObj ? jsonObj : { "resourceType": "Parameters" }, fhirVersion); } addParamStr(name, value) { diff --git a/tx/library/renderer.js b/tx/library/renderer.js index 9058dc4..5648fe4 100644 --- a/tx/library/renderer.js +++ b/tx/library/renderer.js @@ -24,7 +24,7 @@ class Renderer { if (arg.systemUri !== undefined && arg.version !== undefined && arg.code !== undefined && arg.display !== undefined) { // It's a Coding return this.displayCodedCoding(arg); - } else if (arg.coding !== undefined) { + } else if (arg.coding !== undefined || arg.text) { // It's a CodeableConcept return this.displayCodedCodeableConcept(arg); } else if (arg.systemUri !== undefined && arg.version !== undefined) { @@ -74,11 +74,15 @@ class Renderer { displayCodedCodeableConcept(code) { let result = ''; - for (const c of code.coding) { - if (result) { - result = result + ', '; + if (code.text && !code.coding) { + result = '"'+code.text+'"'; + } else { + for (const c of code.coding || []) { + if (result) { + result = result + ', '; + } + result = result + this.displayCodedCoding(c); } - result = result + this.displayCodedCoding(c); } return '[' + result + ']'; } @@ -1280,7 +1284,7 @@ class Renderer { return div_.toString(); } - async renderCapabilityRest(x, rest, cs) { + async renderCapabilityRest(x, rest) { x.h3().tx(`REST ${rest.mode || 'server'} Definition`); if (rest.documentation) { diff --git a/tx/library/terminologycapabilities.js b/tx/library/terminologycapabilities.js index 0abe8b2..be2e8dc 100644 --- a/tx/library/terminologycapabilities.js +++ b/tx/library/terminologycapabilities.js @@ -1,5 +1,5 @@ const {CanonicalResource} = require("./canonical-resource"); -const {VersionUtilities} = require("../../library/version-utilities"); +const {terminologyCapabilitiesToR5, terminologyCapabilitiesFromR5} = require("../xversion/xv-terminologyCapabilities"); /** * Represents a FHIR TerminologyCapabilities resource with version conversion support. @@ -17,7 +17,7 @@ class TerminologyCapabilities extends CanonicalResource { constructor(jsonObj, fhirVersion = 'R5') { super(jsonObj, fhirVersion); // Convert to R5 format internally (modifies input for performance) - this.jsonObj = this._convertToR5(jsonObj, fhirVersion); + this.jsonObj = terminologyCapabilitiesToR5(jsonObj, fhirVersion); this.validate(); this.id = this.jsonObj.id; } @@ -38,7 +38,7 @@ class TerminologyCapabilities extends CanonicalResource { * @returns {string} JSON string */ toJSONString(version = 'R5') { - const outputObj = this._convertFromR5(this.jsonObj, version); + const outputObj = terminologyCapabilitiesFromR5(this.jsonObj, version); return JSON.stringify(outputObj); } @@ -51,246 +51,6 @@ class TerminologyCapabilities extends CanonicalResource { return this._convertFromR5(this.jsonObj, version); } - /** - * Converts input TerminologyCapabilities to R5 format (modifies input object for performance) - * @param {Object} jsonObj - The input TerminologyCapabilities object - * @param {string} version - Source FHIR version - * @returns {Object} The same object, potentially modified to R5 format - * @private - */ - _convertToR5(jsonObj, version) { - if (version === 'R5') { - return jsonObj; // Already R5, no conversion needed - } - - if (version === 'R4') { - // R4 to R5: No major structural changes needed for TerminologyCapabilities - return jsonObj; - } - - if (VersionUtilities.isR3Ver(version)) { - // R3: TerminologyCapabilities doesn't exist - it's a Parameters resource - // Convert from Parameters format to TerminologyCapabilities - return this._convertParametersToR5(jsonObj); - } - - throw new Error(`Unsupported FHIR version: ${version}`); - } - - /** - * Converts R3 Parameters format to R5 TerminologyCapabilities - * @param {Object} params - The Parameters resource - * @returns {Object} TerminologyCapabilities in R5 format - * @private - */ - _convertParametersToR5(params) { - if (params.resourceType !== 'Parameters') { - throw new Error('R3 TerminologyCapabilities must be a Parameters resource'); - } - - const result = { - resourceType: 'TerminologyCapabilities', - id: params.id, - status: 'active', // Default, as Parameters doesn't carry this - kind: 'instance', // Default for terminology server capabilities - codeSystem: [] - }; - - const parameters = params.parameter || []; - let currentSystem = null; - - for (const param of parameters) { - switch (param.name) { - case 'url': - result.url = param.valueUri; - break; - case 'version': - if (currentSystem) { - // This is a code system version - if (param.valueCode) { - currentSystem.version = currentSystem.version || []; - currentSystem.version.push({ code: param.valueCode }); - } - // Empty version parameter means no specific version - } else { - // This is the TerminologyCapabilities version - result.version = param.valueCode || param.valueString; - } - break; - case 'date': - result.date = param.valueDateTime; - break; - case 'system': - // Start a new code system - currentSystem = { uri: param.valueUri }; - result.codeSystem.push(currentSystem); - break; - case 'expansion.parameter': - result.expansion = result.expansion || { parameter: [] }; - result.expansion.parameter.push({ name: param.valueCode }); - break; - } - } - - return result; - } - - /** - * Converts R5 TerminologyCapabilities to target version format (clones object first) - * @param {Object} r5Obj - The R5 format TerminologyCapabilities object - * @param {string} targetVersion - Target FHIR version - * @returns {Object} New object in target version format - * @private - */ - _convertFromR5(r5Obj, targetVersion) { - if (VersionUtilities.isR5Ver(targetVersion)) { - return r5Obj; // No conversion needed - } - - // Clone the object to avoid modifying the original - const cloned = JSON.parse(JSON.stringify(r5Obj)); - - if (VersionUtilities.isR4Ver(targetVersion)) { - return this._convertR5ToR4(cloned); - } else if (VersionUtilities.isR3Ver(targetVersion)) { - return this._convertR5ToR3(cloned); - } - - throw new Error(`Unsupported target FHIR version: ${targetVersion}`); - } - - /** - * Converts R5 TerminologyCapabilities to R4 format - * @param {Object} r5Obj - Cloned R5 TerminologyCapabilities object - * @returns {Object} R4 format TerminologyCapabilities - * @private - */ - _convertR5ToR4(r5Obj) { - // Remove R5-specific elements - if (r5Obj.versionAlgorithmString) { - delete r5Obj.versionAlgorithmString; - } - if (r5Obj.versionAlgorithmCoding) { - delete r5Obj.versionAlgorithmCoding; - } - - // Convert valueCanonical to valueUri throughout the object - this._convertCanonicalToUri(r5Obj); - - return r5Obj; - } - - /** - * Converts R5 TerminologyCapabilities to R3 format (Parameters resource) - * In R3, TerminologyCapabilities didn't exist - we represent it as a Parameters resource - * @param {Object} r5Obj - Cloned R5 TerminologyCapabilities object - * @returns {Object} R3 format Parameters resource - * @private - */ - _convertR5ToR3(r5Obj) { - const params = { - resourceType: 'Parameters', - id: r5Obj.id, - parameter: [] - }; - - // Add url parameter - if (r5Obj.url) { - params.parameter.push({ - name: 'url', - valueUri: r5Obj.url - }); - } - - // Add version parameter - if (r5Obj.version) { - params.parameter.push({ - name: 'version', - valueCode: r5Obj.version - }); - } - - // Add date parameter - if (r5Obj.date) { - params.parameter.push({ - name: 'date', - valueDateTime: r5Obj.date - }); - } - - // Add code systems with their versions - for (const codeSystem of r5Obj.codeSystem || []) { - // Add system parameter - params.parameter.push({ - name: 'system', - valueUri: codeSystem.uri - }); - - // Add version parameter(s) for this code system - if (codeSystem.version && codeSystem.version.length > 0) { - for (const ver of codeSystem.version) { - if (ver.code) { - params.parameter.push({ - name: 'version', - valueCode: ver.code - }); - } else { - // Empty version parameter when no specific version - params.parameter.push({ - name: 'version' - }); - } - } - } else { - // No version specified for this code system - params.parameter.push({ - name: 'version' - }); - } - } - - // Add expansion parameters - if (r5Obj.expansion && r5Obj.expansion.parameter) { - for (const expParam of r5Obj.expansion.parameter) { - params.parameter.push({ - name: 'expansion.parameter', - valueCode: expParam.name - }); - } - } - - return params; - } - - /** - * Recursively converts valueCanonical to valueUri in an object - * R3/R4 doesn't have canonical type in the same way, so valueCanonical must become valueUri - * @param {Object} obj - Object to convert - * @private - */ - _convertCanonicalToUri(obj) { - if (!obj || typeof obj !== 'object') { - return; - } - - if (Array.isArray(obj)) { - obj.forEach(item => this._convertCanonicalToUri(item)); - return; - } - - // Convert valueCanonical to valueUri - if (obj.valueCanonical !== undefined) { - obj.valueUri = obj.valueCanonical; - delete obj.valueCanonical; - } - - // Recurse into all properties - for (const key of Object.keys(obj)) { - if (typeof obj[key] === 'object') { - this._convertCanonicalToUri(obj[key]); - } - } - } /** * Validates that this is a proper TerminologyCapabilities resource diff --git a/tx/library/valueset.js b/tx/library/valueset.js index bc2d8f1..3b632ce 100644 --- a/tx/library/valueset.js +++ b/tx/library/valueset.js @@ -1,6 +1,5 @@ const {CanonicalResource} = require("./canonical-resource"); -const {getValueName} = require("../../library/utilities"); -const {VersionUtilities} = require("../../library/version-utilities"); +const {valueSetToR5, valueSetFromR5} = require("../xversion/xv-valueset"); /** * Represents a FHIR ValueSet resource with version conversion support @@ -23,7 +22,7 @@ class ValueSet extends CanonicalResource { constructor(jsonObj, fhirVersion = 'R5') { super(jsonObj, fhirVersion); // Convert to R5 format internally (modifies input for performance) - this.jsonObj = this._convertToR5(jsonObj, fhirVersion); + this.jsonObj = valueSetToR5(jsonObj, fhirVersion); this.validate(); this.buildMaps(); } @@ -51,241 +50,10 @@ class ValueSet extends CanonicalResource { * @returns {string} JSON string */ toJSONString(version = 'R5') { - const outputObj = this.convertFromR5(this.jsonObj, version); + const outputObj = valueSetFromR5(this.jsonObj, version); return JSON.stringify(outputObj); } - /** - * Converts input ValueSet to R5 format (modifies input object for performance) - * @param {Object} jsonObj - The input ValueSet object - * @param {string} version - Source FHIR version - * @returns {Object} The same object, potentially modified to R5 format - * @private - */ - _convertToR5(jsonObj, version) { - if (version === 'R5') { - return jsonObj; // Already R5, no conversion needed - } - - if (version === 'R3') { - // R3 to R5: Remove extensible field (we ignore it completely) - if (jsonObj.extensible !== undefined) { - delete jsonObj.extensible; - } - return jsonObj; - } - - if (version === 'R4') { - // R4 to R5: No structural conversion needed - // R5 is backward compatible for the structural elements we care about - return jsonObj; - } - - throw new Error(`Unsupported FHIR version: ${version}`); - } - - /** - * Converts R5 ValueSet to target version format (clones object first) - * @param {Object} r5Obj - The R5 format ValueSet object - * @param {string} targetVersion - Target FHIR version - * @returns {Object} New object in target version format - * @private - */ - convertFromR5(r5Obj, targetVersion) { - if (VersionUtilities.isR5Ver(targetVersion)) { - return r5Obj; // No conversion needed - } - - // Clone the object to avoid modifying the original - const cloned = JSON.parse(JSON.stringify(r5Obj)); - - if (VersionUtilities.isR4Ver(targetVersion)) { - return this._convertR5ToR4(cloned); - } else if (VersionUtilities.isR3Ver(targetVersion)) { - return this._convertR5ToR3(cloned); - } - - throw new Error(`Unsupported target FHIR version: ${targetVersion}`); - } - - /** - * Converts R5 ValueSet to R4 format - * @param {Object} r5Obj - Cloned R5 ValueSet object - * @returns {Object} R4 format ValueSet - * @private - */ - _convertR5ToR4(r5Obj) { - // Remove R5-specific elements that don't exist in R4 - if (r5Obj.versionAlgorithmString) { - delete r5Obj.versionAlgorithmString; - } - if (r5Obj.versionAlgorithmCoding) { - delete r5Obj.versionAlgorithmCoding; - } - - // Filter out R5-only filter operators in compose - if (r5Obj.compose && r5Obj.compose.include) { - r5Obj.compose.include = r5Obj.compose.include.map(include => { - if (include.filter && Array.isArray(include.filter)) { - include.filter = include.filter.map(filter => { - if (filter.op && this._isR5OnlyFilterOperator(filter.op)) { - // Remove R5-only operators - return null; - } - return filter; - }).filter(filter => filter !== null); - } - return include; - }); - } - - if (r5Obj.compose && r5Obj.compose.exclude) { - r5Obj.compose.exclude = r5Obj.compose.exclude.map(exclude => { - if (exclude.filter && Array.isArray(exclude.filter)) { - exclude.filter = exclude.filter.map(filter => { - if (filter.op && this._isR5OnlyFilterOperator(filter.op)) { - // Remove R5-only operators - return null; - } - return filter; - }).filter(filter => filter !== null); - } - return exclude; - }); - } - - if (r5Obj.expansion) { - let exp = r5Obj.expansion; - - // Convert ValueSet.expansion.property to extensions - if (exp.property && exp.property.length > 0) { - exp.extension = exp.extension || []; - for (let prop of exp.property) { - exp.extension.push({ - url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.property", - extension: [ - { url: "code", valueCode: prop.code }, - { url: "uri", valueUri: prop.uri } - ] - }); - } - delete exp.property; - this.convertContainsPropertyR5ToR4(exp.contains); - - } - } - - return r5Obj; - } - - // Recursive function to convert contains.property - convertContainsPropertyR5ToR4(containsList) { - if (!containsList) return; - - for (let item of containsList) { - if (item.property && item.property.length > 0) { - item.extension = item.extension || []; - for (let prop of item.property) { - let ext = { - url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.contains.property", - extension: [ - { url: "code", valueCode: prop.code } - ] - }; - let pn = getValueName(prop); - let subExt = { url: "value" }; - subExt[pn] = prop[pn]; - ext.extension.push(subExt); - item.extension.push(ext); - } - delete item.property; - } - - // Recurse into nested contains - if (item.contains) { - this.convertContainsPropertyR5ToR4(item.contains); - } - } - } - - /** - * Converts R5 ValueSet to R3 format - * @param {Object} r5Obj - Cloned R5 ValueSet object - * @returns {Object} R3 format ValueSet - * @private - */ - _convertR5ToR3(r5Obj) { - // First apply R4 conversions - const r4Obj = this._convertR5ToR4(r5Obj); - - // R3 has more limited filter operator support - if (r4Obj.compose && r4Obj.compose.include) { - r4Obj.compose.include = r4Obj.compose.include.map(include => { - if (include.filter && Array.isArray(include.filter)) { - include.filter = include.filter.map(filter => { - if (filter.op && !this._isR3CompatibleFilterOperator(filter.op)) { - // Remove non-R3-compatible operators - return null; - } - return filter; - }).filter(filter => filter !== null); - } - return include; - }); - } - - if (r4Obj.compose && r4Obj.compose.exclude) { - r4Obj.compose.exclude = r4Obj.compose.exclude.map(exclude => { - if (exclude.filter && Array.isArray(exclude.filter)) { - exclude.filter = exclude.filter.map(filter => { - if (filter.op && !this._isR3CompatibleFilterOperator(filter.op)) { - // Remove non-R3-compatible operators - return null; - } - return filter; - }).filter(filter => filter !== null); - } - return exclude; - }); - } - - return r4Obj; - } - - /** - * Checks if a filter operator is R5-only - * @param {string} operator - Filter operator code - * @returns {boolean} True if operator is R5-only - * @private - */ - _isR5OnlyFilterOperator(operator) { - const r5OnlyOperators = [ - 'generalizes', // Added in R5 - // Add other R5-only operators as they're identified - ]; - return r5OnlyOperators.includes(operator); - } - - /** - * Checks if a filter operator is compatible with R3 - * @param {string} operator - Filter operator code - * @returns {boolean} True if operator is R3-compatible - * @private - */ - _isR3CompatibleFilterOperator(operator) { - const r3CompatibleOperators = [ - '=', // Equal - 'is-a', // Is-A relationship - 'descendent-of', // Descendant of (note: R3 spelling) - 'is-not-a', // Is-Not-A relationship - 'regex', // Regular expression - 'in', // In set - 'not-in', // Not in set - 'exists', // Property exists - ]; - return r3CompatibleOperators.includes(operator); - } - /** * Gets the FHIR version this ValueSet was loaded from * @returns {string} FHIR version ('R3', 'R4', or 'R5') diff --git a/tx/params.js b/tx/params.js index b7332be..d5c4ac6 100644 --- a/tx/params.js +++ b/tx/params.js @@ -34,6 +34,8 @@ class TxParameters { limit = -1; offset = -1; validating = false; + abstractOk = true; // note true! + inferSystem = false; constructor(languages, i18n, validating) { validateParameter(languages, 'languages', LanguageDefinitions); @@ -45,6 +47,7 @@ class TxParameters { this.FVersionRules = []; this.FProperties = []; this.FDesignations = []; + this.supplements = new Set; this.FGenerateNarrative = true; this.FHTTPLanguages = null; @@ -118,7 +121,7 @@ class TxParameters { try { this.DisplayLanguages = Languages.fromAcceptLanguage(getValuePrimitive(p), this.languageDefinitions, !this.validating); } catch (error) { - throw new Issue("error", "processing", null, 'INVALID_DISPLAY_NAME', this.i18n.translate('INVALID_DISPLAY_NAME', this.HTTPLanguages, getValuePrimitive(p))).handleAsOO(400); + throw new Issue("error", "processing", null, 'INVALID_DISPLAY_NAME', this.i18n.translate('INVALID_DISPLAY_NAME', this.HTTPLanguages, [getValuePrimitive(p)])).handleAsOO(400); } break; } @@ -198,6 +201,7 @@ class TxParameters { if (value && (value.resourceType === 'Parameters' || value.resourceType === 'ExpansionProfile')) { this.readParams(value); } + break; } // eslint-disable-next-line no-fallthrough case 'term': // jQuery support @@ -218,6 +222,24 @@ class TxParameters { this.limit = Utilities.parseIntOrDefault(getValuePrimitive(p), -1); break; } + + case 'useSupplement' : { + this.supplements.add(getValuePrimitive(p)); + break; + } + + case 'abstract': { + if (getValuePrimitive(p) == true) { + this.abstractOk = true; + } else if (getValuePrimitive(p) == false) { + this.abstractOk = false; + } + break; + } + case 'inferSystem': { + if (getValuePrimitive(p) == true) this.inferSystem = true; + break; + } } } diff --git a/tx/tests/testcases-generator.js b/tx/tests/testcases-generator.js index 44fc641..f01c880 100644 --- a/tx/tests/testcases-generator.js +++ b/tx/tests/testcases-generator.js @@ -63,7 +63,7 @@ describe('Tx Tests', () => { } for (const test of suite.tests) { - if (!test.mode || modes.has(test.mode)) { + if ((!test.mode || modes.has(test.mode)) && (!test["full-set"])) { let testDetails = { suite: suite.name, test: test.name diff --git a/tx/tx-html.js b/tx/tx-html.js index db0c807..bbf8dc0 100644 --- a/tx/tx-html.js +++ b/tx/tx-html.js @@ -102,8 +102,20 @@ class TxHtmlRenderer { * Check if request accepts HTML */ acceptsHtml(req) { - const accept = req.headers.accept || ''; - return accept.includes('text/html'); + let _fmt = req.query._format; + if (_fmt && typeof _fmt !== 'string') { + _fmt = null + } + if (_fmt && _fmt == 'html') { + return true; + } + if (!_fmt) { + _fmt = req.headers.accept || ''; + } + if (typeof _fmt !== 'string') { + return false; + } + return _fmt.includes('text/html'); } /** diff --git a/tx/tx.js b/tx/tx.js index 50d38d6..b3242cc 100644 --- a/tx/tx.js +++ b/tx/tx.js @@ -37,6 +37,16 @@ const {TxHtmlRenderer} = require("./tx-html"); const {Renderer} = require("./library/renderer"); const {OperationsWorker} = require("./workers/operations"); const {RelatedWorker} = require("./workers/related"); +const {codeSystemFromR5} = require("./xversion/xv-codesystem"); +const {operationOutcomeFromR5} = require("./xversion/xv-operationoutcome"); +const {parametersFromR5} = require("./xversion/xv-parameters"); +const {conceptMapFromR5} = require("./xversion/xv-conceptmap"); +const {valueSetFromR5} = require("./xversion/xv-valueset"); +const {terminologyCapabilitiesFromR5} = require("./xversion/xv-terminologyCapabilities"); +const {capabilityStatementFromR5} = require("./xversion/xv-capabiliityStatement"); +const {bundleFromR5} = require("./xversion/xv-bundle"); +const {convertResourceToR5} = require("./xversion/xv-resource"); +const ClosureWorker = require("./workers/closure"); // const {writeFileSync} = require("fs"); class TXModule { @@ -71,10 +81,49 @@ class TXModule { } acceptsXml(req) { - const accept = req.headers.accept || ''; - return accept.includes('application/fhir+xml') || accept.includes('application/xml+fhir'); + let _fmt = req.query._format; + if (_fmt && typeof _fmt !== 'string') { + _fmt = null + } + + if (_fmt && _fmt == 'xml') { + return 'application/fhir+xml'; + } + if (!_fmt) { + _fmt = req.headers.accept || ''; + } + if (_fmt.includes('application/fhir+xml')) { + return 'application/fhir+xml'; + } else if (_fmt.includes('application/xml+fhir')) { + return 'application/xml+fhir'; + } else if (_fmt.includes('application/xml')) { + return 'application/xml'; + } else { + return null; + } } + acceptsJson(req) { + let _fmt = req.query._format; + if (_fmt && typeof _fmt !== 'string') { + _fmt = null + } + if (_fmt && _fmt == 'json') { + return 'application/fhir+json'; + } + if (!_fmt) { + _fmt = req.headers.accept || ''; + } + if (_fmt.includes('application/fhir+json')) { + return 'application/fhir+json'; + } else if (_fmt.includes('application/json+fhir')) { + return 'application/json+fhir'; + } else if (_fmt.includes('application/json')) { + return 'application/json'; + } else { + return 'application/fhir+json'; + } + } /** * Initialize the TX module @@ -105,9 +154,9 @@ class TXModule { } // Load language definitions - const langPath = path.join(__dirname, 'data', 'lang.dat'); + const langPath = path.join(__dirname, 'data'); this.log.info(`Loading language definitions from: ${langPath}`); - this.languages = await LanguageDefinitions.fromFile(langPath); + this.languages = await LanguageDefinitions.fromFiles(langPath); this.log.info('Language definitions loaded'); // Initialize i18n support @@ -246,7 +295,9 @@ class TXModule { try { const duration = Date.now() - req.txStartTime; const isHtml = txhtml.acceptsHtml(req); - const isXml = this.acceptsXml(req); + const xmlFmt = this.acceptsXml(req); + const jsonFmt = this.acceptsJson(req); + data = this.transformResourceForVersion(data, endpointInfo.fhirVersion); let responseSize; let result; @@ -258,29 +309,31 @@ class TXModule { responseSize = Buffer.byteLength(html, 'utf8'); res.setHeader('Content-Type', 'text/html'); result = res.send(html); - } else if (isXml) { + } else if (xmlFmt) { try { const xml = this.convertResourceToXml(data); responseSize = Buffer.byteLength(xml, 'utf8'); - res.setHeader('Content-Type', 'application/fhir+xml'); + res.setHeader('Content-Type', xmlFmt); result = res.send(xml); } catch (err) { console.error(err); // Fall back to JSON if XML conversion not supported this.log.warn(`XML conversion failed for ${data.resourceType}: ${err.message}, falling back to JSON`); + res.setHeader('Content-Type', jsonFmt); const jsonStr = JSON.stringify(data); responseSize = Buffer.byteLength(jsonStr, 'utf8'); result = originalJson(data); } } else { const jsonStr = JSON.stringify(data); + res.setHeader('Content-Type', jsonFmt); this.checkProperJson(jsonStr); responseSize = Buffer.byteLength(jsonStr, 'utf8'); result = originalJson(data); } // Log the request with request ID - const format = isHtml ? 'html' : (isXml ? 'xml' : 'json'); + const format = isHtml ? 'html' : (xmlFmt ? 'xml' : 'json'); let li = req.logInfo ? "(" + req.logInfo + ")" : ""; this.log.info(`[${requestId}] ${req.method} ${format} ${res.statusCode} ${duration}ms ${responseSize}: ${req.originalUrl} ${li})`); @@ -367,8 +420,20 @@ class TXModule { }); } } + } else if (contentType != 'application/x-www-form-urlencoded') { + return res.status(415).json({ + resourceType: 'OperationOutcome', + issue: [{ + severity: 'error', + code: 'invalid', + diagnostics: `Unsupported Media Type: ${contentType}` + }] + }); } + if (req.body) { + req.body = convertResourceToR5(req.body, req.txEndpoint.fhirVersion); + } next(); }); @@ -577,7 +642,7 @@ class TXModule { router.get('/ConceptMap/\\$closure', async (req, res) => { const start = Date.now(); try { - let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n); + let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n); await worker.handle(req, res, this.log); } finally { this.countRequest('$closure', Date.now() - start); @@ -586,7 +651,7 @@ class TXModule { router.post('/ConceptMap/\\$closure', async (req, res) => { const start = Date.now(); try { - let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n); + let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n); await worker.handle(req, res, this.log); } finally { this.countRequest('$closure', Date.now() - start); @@ -981,6 +1046,24 @@ class TXModule { // throw new Error(errors.join('; ')); // } } + + transformResourceForVersion(data, fhirVersion) { + if (fhirVersion == "5.0" || !data.resourceType) { + return data; + } + switch (data.resourceType) { + case "CodeSystem": return codeSystemFromR5(data, fhirVersion); + case "CapabilityStatement": return capabilityStatementFromR5(data, fhirVersion); + case "TerminologyCapabilities": return terminologyCapabilitiesFromR5(data, fhirVersion); + case "ValueSet": return valueSetFromR5(data, fhirVersion); + case "ConceptMap": return conceptMapFromR5(data, fhirVersion); + case "Parameters": return parametersFromR5(data, fhirVersion); + case "OperationOutcome": return operationOutcomeFromR5(data, fhirVersion); + case "Bundle": return bundleFromR5(data, fhirVersion); + default: return data; + } + } + } module.exports = TXModule; \ No newline at end of file diff --git a/tx/workers/expand.js b/tx/workers/expand.js index ea286b9..6c96100 100644 --- a/tx/workers/expand.js +++ b/tx/workers/expand.js @@ -200,6 +200,8 @@ class ValueSetExpander { params; excluded = new Set(); hasExclusions = false; + requiredSupplements = new Set(); + usedSupplements = new Set(); constructor(worker, params) { this.worker = worker; @@ -615,7 +617,8 @@ class ValueSetExpander { } if (cset.system) { - const cs = await this.worker.findCodeSystem(cset.system, cset.version, this.params, ['complete', 'fragment'], false, true, true, null); + const cs = await this.worker.findCodeSystem(cset.system, cset.version, this.params, ['complete', 'fragment'], + false, true, true, null, this.requiredSupplements); this.worker.seeSourceProvider(cs, cset.system); if (cs == null) { // nothing @@ -677,13 +680,13 @@ class ValueSetExpander { } else { const filters = []; const prep = null; - const cs = await this.worker.findCodeSystem(cset.system, cset.version, this.params, ['complete', 'fragment'], false, false, true, null); + const cs = await this.worker.findCodeSystem(cset.system, cset.version, this.params, ['complete', 'fragment'], + false, false, true, null, this.requiredSupplements); if (cs == null) { // nothing } else { - - this.worker.checkSupplements(cs, cset, this.requiredSupplements); + this.worker.checkSupplements(cs, cset, this.requiredSupplements, this.usedSupplements); this.checkProviderCanonicalStatus(expansion, cs, this.valueSet); const sv = this.canonical(await cs.system(), await cs.version()); this.addParamUri(expansion, 'used-codesystem', sv); @@ -815,8 +818,7 @@ class ValueSetExpander { const fset = await cs.executeFilters(prep); if (await cs.filtersNotClosed(prep)) { notClosed.value = true; - } - if (fset.length === 1 && !excludeInactive && !this.params.activeOnly) { + } else if (fset.length === 1 && !excludeInactive && !this.params.activeOnly) { this.addToTotal(await cs.filterSize(prep, fset[0])); } @@ -900,9 +902,10 @@ class ValueSetExpander { } else { const filters = []; const prep = null; - const cs = await this.worker.findCodeSystem(cset.system, cset.version, this.params, ['complete', 'fragment'], false, true, true, null); + const cs = await this.worker.findCodeSystem(cset.system, cset.version, this.params, ['complete', 'fragment'], false, + true, true, null, this.requiredSupplements); - this.worker.checkSupplements(cs, cset, this.requiredSupplements); + this.worker.checkSupplements(cs, cset, this.requiredSupplements, this.usedSupplements); this.checkResourceCanonicalStatus(expansion, cs, this.valueSet); const sv = this.canonical(await cs.system(), await cs.version()); this.addParamUri(expansion, 'used-codesystem', sv); @@ -1160,9 +1163,9 @@ class ValueSetExpander { result.text = undefined; } - this.requiredSupplements = []; + for (let s of this.params.supplements) this.requiredSupplements.add(s); for (const ext of Extensions.list(source.jsonObj, 'http://hl7.org/fhir/StructureDefinition/valueset-supplement')) { - this.requiredSupplements.push(getValuePrimitive(ext)); + this.requiredSupplements.add(getValuePrimitive(ext)); } if (result.expansion) { @@ -1261,8 +1264,9 @@ class ValueSetExpander { await this.handleCompose(source, filter, exp, notClosed); } - if (this.requiredSupplements.length > 0) { - throw new Issue('error', 'not-found', null, 'VALUESET_SUPPLEMENT_MISSING', this.worker.opContext.i18n.translatePlural(this.requiredSupplements.length, 'VALUESET_SUPPLEMENT_MISSING', this.params.httpLanguages, [this.requiredSupplements.join(', ')]), 'not-found', 400); + const unused = new Set([...this.requiredSupplements].filter(s => !this.usedSupplements.has(s))); + if (unused.size > 0) { + throw new Issue('error', 'not-found', null, 'VALUESET_SUPPLEMENT_MISSING', this.worker.i18n.translatePlural(unused.size, 'VALUESET_SUPPLEMENT_MISSING', this.params.HTTPLanguages, [[...unused].join(',')]), 'not-found').handleAsOO(400); } } catch (e) { if (e instanceof Issue) { @@ -1517,6 +1521,7 @@ class ValueSetExpander { } class ExpandWorker extends TerminologyWorker { + /** * @param {OperationContext} opContext - Operation context * @param {Logger} log - Logger instance @@ -1552,10 +1557,10 @@ class ExpandWorker extends TerminologyWorker { if (error instanceof Issue) { let oo = new OperationOutcome(); oo.addIssue(error); - return res.status(error.statusCode || 500).json(this.fixForVersion(oo.jsonObj)); + return res.status(error.statusCode || 500).json(oo.jsonObj); } else { const issueCode = error.issueCode || 'exception'; - return res.status(statusCode).json(this.fixForVersion({ + return res.status(statusCode).json({ resourceType: 'OperationOutcome', issue: [{ severity: 'error', @@ -1565,7 +1570,7 @@ class ExpandWorker extends TerminologyWorker { }, diagnostics: error.message }] - })); + }); } } } @@ -1584,7 +1589,7 @@ class ExpandWorker extends TerminologyWorker { this.log.error(error); const statusCode = error.statusCode || 500; const issueCode = error.issueCode || 'exception'; - return res.status(statusCode).json(this.fixForVersion({ + return res.status(statusCode).json({ resourceType: 'OperationOutcome', issue: [{ severity: 'error', @@ -1594,7 +1599,7 @@ class ExpandWorker extends TerminologyWorker { }, diagnostics: error.message }] - })); + }); } } @@ -1675,7 +1680,7 @@ class ExpandWorker extends TerminologyWorker { // Perform the expansion const result = await this.doExpand(valueSet, txp, logExtraOutput); req.logInfo = this.usedSources.join("|")+txp.logInfo(); - return res.json(this.fixForVersion(result)); + return res.json(result); } /** @@ -1725,7 +1730,7 @@ class ExpandWorker extends TerminologyWorker { // Perform the expansion const result = await this.doExpand(valueSet, txp, logExtraOutput); req.logInfo = this.usedSources.join("|")+txp.logInfo(); - return res.json(this.fixForVersion(result)); + return res.json(result); } // Note: setupAdditionalResources, queryToParameters, formToParameters, diff --git a/tx/workers/lookup.js b/tx/workers/lookup.js index 1362e3c..fdf0464 100644 --- a/tx/workers/lookup.js +++ b/tx/workers/lookup.js @@ -126,7 +126,8 @@ class LookupWorker extends TerminologyWorker { } else if (params.has('system') && params.has('code')) { // system + code parameters - csProvider = await this.findCodeSystem(params.get('system'), params.get('version') || '', txp, ['complete', 'fragment'], true); + csProvider = await this.findCodeSystem(params.get('system'), params.get('version') || '', txp, ['complete', 'fragment'], + null, true, false, false, txp.supplements); this.seeSourceProvider(csProvider, params.get('system')); code = params.get('code'); @@ -141,7 +142,15 @@ class LookupWorker extends TerminologyWorker { const msg = versionStr ? `CodeSystem not found: ${systemUrl} version ${versionStr}` : `CodeSystem not found: ${systemUrl}`; - return res.status(404).json(this.operationOutcome('error', 'not-found', msg)); + return res.status(422).json(this.operationOutcome('error', 'not-found', msg)); + } + + // check supplements + const used = new Set(); + this.checkSupplements(csProvider, null, txp.supplements, used); + const unused = new Set([...txp.supplements].filter(s => !used.has(s))); + if (unused.size > 0) { + throw new Issue('error', 'not-found', null, 'VALUESET_SUPPLEMENT_MISSING', this.i18n.translatePlural(unused.size, 'VALUESET_SUPPLEMENT_MISSING', txp.HTTPLanguages, [[...unused].join(',')]), 'not-found').handleAsOO(400); } // Perform the lookup @@ -202,7 +211,7 @@ class LookupWorker extends TerminologyWorker { } // Load any supplements - const supplements = this.loadSupplements(codeSystem.url, codeSystem.version); + const supplements = this.loadSupplements(codeSystem.url, codeSystem.version, txp.supplements); // Create a FhirCodeSystemProvider for this CodeSystem const csProvider = new FhirCodeSystemProvider(this.opContext, codeSystem, supplements); @@ -234,6 +243,8 @@ class LookupWorker extends TerminologyWorker { async doLookup(csProvider, code, params) { this.deadCheck('doLookup'); + await this.checkSupplements(csProvider, null, params.supplements); + // Helper to check if a property should be included const hasProp = (name, defaultValue = true) => { if (!params.properties || params.properties.length === 0) { @@ -386,7 +397,6 @@ class LookupWorker extends TerminologyWorker { }); } - /** * Build an OperationOutcome * @param {string} severity - error, warning, information @@ -400,7 +410,7 @@ class LookupWorker extends TerminologyWorker { issue: [{ severity, code, - diagnostics: message + details: {text : message} }] }; } diff --git a/tx/workers/metadata.js b/tx/workers/metadata.js index add654f..6c4421c 100644 --- a/tx/workers/metadata.js +++ b/tx/workers/metadata.js @@ -37,13 +37,13 @@ class MetadataHandler { if (mode === 'terminology') { this.logInfo = 'termcaps'; const tc = new TerminologyCapabilities(await this.buildTerminologyCapabilities(endpoint, provider)); - return res.json(tc.toJSON(endpoint.fhirVersion)); + return res.json(tc.jsonObj); } this.logInfo = 'metadata'; // Default: return CapabilityStatement const cs = new CapabilityStatement(this.buildCapabilityStatement(endpoint, provider)); - return res.json(cs.toJSON(endpoint.fhirVersion)); + return res.json(cs.jsonObj); } /** diff --git a/tx/workers/related.js b/tx/workers/related.js index ef53eaa..55a4392 100644 --- a/tx/workers/related.js +++ b/tx/workers/related.js @@ -52,10 +52,10 @@ class RelatedWorker extends TerminologyWorker { if (error instanceof Issue) { let oo = new OperationOutcome(); oo.addIssue(error); - return res.status(error.statusCode || 500).json(this.fixForVersion(oo.jsonObj)); + return res.status(error.statusCode || 500).json(oo.jsonObj); } else { const issueCode = error.issueCode || 'exception'; - return res.status(statusCode).json(this.fixForVersion({ + return res.status(statusCode).json({ resourceType: 'OperationOutcome', issue: [{ severity: 'error', @@ -65,7 +65,7 @@ class RelatedWorker extends TerminologyWorker { }, diagnostics: error.message }] - })); + }); } } } @@ -84,7 +84,7 @@ class RelatedWorker extends TerminologyWorker { this.log.error(error); const statusCode = error.statusCode || 500; const issueCode = error.issueCode || 'exception'; - return res.status(statusCode).json(this.fixForVersion({ + return res.status(statusCode).json({ resourceType: 'OperationOutcome', issue: [{ severity: 'error', @@ -94,7 +94,7 @@ class RelatedWorker extends TerminologyWorker { }, diagnostics: error.message }] - })); + }); } } @@ -115,7 +115,7 @@ class RelatedWorker extends TerminologyWorker { let otherVS = await this.readValueSet(res, "other", params, txp); const result = await this.doRelated(txp, thisVS, otherVS); - return res.json(this.fixForVersion(result)); + return res.json(result); } /** @@ -141,7 +141,7 @@ class RelatedWorker extends TerminologyWorker { let otherVS = await this.readValueSet(res, "other", params, txp); const result = await this.doRelated(txp, thisVS, otherVS); - return res.json(this.fixForVersion(result)); + return res.json(result); } /** diff --git a/tx/workers/subsumes.js b/tx/workers/subsumes.js index 08cfd66..55fd1ae 100644 --- a/tx/workers/subsumes.js +++ b/tx/workers/subsumes.js @@ -167,7 +167,7 @@ class SubsumesWorker extends TerminologyWorker { txp.readParams(params.jsonObj); // Load any supplements - const supplements = this.loadSupplements(codeSystem.url, codeSystem.version); + const supplements = this.loadSupplements(codeSystem.url, codeSystem.version, txp.supplements); // Create a FhirCodeSystemProvider for this CodeSystem const csProvider = new FhirCodeSystemProvider(this.opContext, codeSystem, supplements); @@ -298,8 +298,16 @@ class SubsumesWorker extends TerminologyWorker { throw error; } + let equal = false; + if (csProvider.isCaseSensitive()) { + equal = codingA.code == codingB.code; + } else { + equal = codingA.code === codingB.code; + } + equal = equal || locateA == locateB; + // Determine the subsumption relationship - let outcome = await csProvider.subsumesTest(codingA.code, codingB.code); + let outcome = equal ? 'equivalent' : await csProvider.subsumesTest(codingA.code, codingB.code); return { resourceType: 'Parameters', diff --git a/tx/workers/translate.js b/tx/workers/translate.js index 5e6b2f4..2ac731b 100644 --- a/tx/workers/translate.js +++ b/tx/workers/translate.js @@ -247,7 +247,7 @@ class TranslateWorker extends TerminologyWorker { checkCode(op, langList, path, code, system, version, display) { let result = false; - const cp = this.findCodeSystem(system, version, null, ['complete', 'fragment'], true, true, false, null); + const cp = this.findCodeSystem(system, version, null, ['complete', 'fragment'], true, true, false, null, this.requiredSupplements); if (cp != null) { const lct = cp.locate(this.opContext, code); if (op.error('InstanceValidator', 'invalid', path, lct != null, 'Unknown Code (' + system + '#' + code + ')')) { diff --git a/tx/workers/validate.js b/tx/workers/validate.js index 044fb1d..f8a83c9 100644 --- a/tx/workers/validate.js +++ b/tx/workers/validate.js @@ -43,7 +43,6 @@ class ValueSetChecker { worker; valueSet; params; - requiredSupplements = []; others = new Map(); constructor(worker, valueSet, params) { @@ -151,6 +150,7 @@ class ValueSetChecker { async determineSystem(opContext, code, systems, op) { let result = ''; let needDoExpansion = false; + let mayNeedExpansion = false; for (let vsi of this.valueSet.jsonObj.compose.exclude || []) { if (vsi.valueSet || !vsi.system || vsi.filter) { @@ -158,9 +158,12 @@ class ValueSetChecker { } } for (let vsi of this.valueSet.jsonObj.compose.include || []) { - if (vsi.valueSet || !vsi.system || vsi.filter) { + if (vsi.valueSet || !vsi.system) { needDoExpansion = true; } + if (vsi.filter) { + mayNeedExpansion = true; + } } if (needDoExpansion) { @@ -168,7 +171,7 @@ class ValueSetChecker { } else { for (let vsi of this.valueSet.jsonObj.compose.include) { this.worker.deadCheck('determineSystem'); - let cs = await this.worker.findCodeSystem(vsi.system, '', null, ['complete', 'fragment'], op,true); + let cs = await this.worker.findCodeSystem(vsi.system, '', null, ['complete', 'fragment'], op,true, false, false, this.worker.requiredSupplements); if (cs === null) { return ''; } @@ -192,7 +195,11 @@ class ValueSetChecker { if (!result) { result = vsi.system; } else if (result !== vsi.system) { - return ''; + if (mayNeedExpansion) { + result = await this.determineSystemFromExpansion(code, systems); + } else { + return ''; + } } } } @@ -212,7 +219,7 @@ class ValueSetChecker { let result; - let csa = await this.worker.findCodeSystem(system, '', this.params, "*", op,true, false, true); + let csa = await this.worker.findCodeSystem(system, '', this.params, "*", op,true, false, true, this.worker.requiredSupplements); result = this.worker.determineVersionBase(system, versionVS, this.params); @@ -231,7 +238,7 @@ class ValueSetChecker { } } - let cs = await this.worker.findCodeSystem(system, result, this.params, ['complete', 'fragment'], op,true, false, false); + let cs = await this.worker.findCodeSystem(system, result, this.params, ['complete', 'fragment'], op,true, false, false, this.worker.requiredSupplements); if (cs !== null && cs.version() !== versionCoding && !cs.versionIsMoreDetailed(versionCoding, cs.version())) { let errLvl = 'error'; let msg, mid; @@ -252,7 +259,7 @@ class ValueSetChecker { if (errLvl === 'error') { messages.push(msg); } - let cs2 = await this.worker.findCodeSystem(system, versionCoding, this.params, ['complete', 'fragment'], op, true, false, true); + let cs2 = await this.worker.findCodeSystem(system, versionCoding, this.params, ['complete', 'fragment'], op, true, false, true, this.worker.requiredSupplements); if (cs2 !== null) { cs2 = null; } else { @@ -300,11 +307,7 @@ class ValueSetChecker { this.worker.opContext.addNote(this.valueSet, 'Version Rules: ' + vrs, this.indentCount); } } - this.requiredSupplements = []; - for (let ext of Extensions.list(this.valueSet.jsonObj, 'http://hl7.org/fhir/StructureDefinition/valueset-supplement')) { - this.requiredSupplements.push(getValuePrimitive(ext)); - } - if (this.requiredSupplements.length > 0) { + if (this.worker.requiredSupplements.size > 0) { await this.checkSupplementsExist(this.valueSet); } @@ -328,8 +331,10 @@ class ValueSetChecker { } } } - if (this.requiredSupplements.length > 0) { - throw new Issue('error', 'not-found', null, 'VALUESET_SUPPLEMENT_MISSING', this.worker.i18n.translatePlural(this.requiredSupplements.length, 'VALUESET_SUPPLEMENT_MISSING', this.params.HTTPLanguages, [this.requiredSupplements.join(',')])).handleAsOO(400); + + const unused = new Set([...this.worker.requiredSupplements].filter(s => !this.worker.usedSupplements.has(s))); + if (unused.size > 0) { + throw new Issue('error', 'not-found', null, 'VALUESET_SUPPLEMENT_MISSING', this.worker.i18n.translatePlural(unused.size, 'VALUESET_SUPPLEMENT_MISSING', this.params.HTTPLanguages, [[...unused].join(',')]), 'not-found').handleAsOO(400); } } @@ -354,12 +359,12 @@ class ValueSetChecker { } } let v = this.worker.determineVersionBase(cc.system, cc.version, this.params); - let cs = await this.worker.findCodeSystem(cc.system, v, this.params, ['complete', 'fragment'], null, true, false, false); + let cs = await this.worker.findCodeSystem(cc.system, v, this.params, ['complete', 'fragment'], null, true, false, false, this.worker.requiredSupplements); if (cs !== null) { this.worker.opContext.addNote(this.valueSet, 'CodeSystem found: "' + this.worker.renderer.displayCoded(cs) + '"', this.indentCount); - for (let i = this.requiredSupplements.length - 1; i >= 0; i--) { - if (cs.hasSupplement(this.requiredSupplements[i])) { - this.requiredSupplements.splice(i, 1); + for (const s of this.worker.requiredSupplements) { + if (cs.hasSupplement(s)) { + this.worker.usedSupplements.add(s); } } let i = 0; @@ -416,9 +421,9 @@ class ValueSetChecker { } } - async checkSimple(issuePath, system, version, code, abstractOk, inferSystem, op) { + async checkSimple(issuePath, system, version, code, op) { this.worker.opContext.clearContexts(); - if (inferSystem) { + if (this.params.inferSystem) { this.worker.opContext.addNote(this.valueSet, 'Validate "' + code + '" and infer system', this.indentCount); } else { this.worker.opContext.addNote(this.valueSet, 'Validate "' + this.worker.renderer.displayCoded(system, version, code) + '"', this.indentCount); @@ -434,13 +439,13 @@ class ValueSetChecker { let contentMode = {value: null}; let impliedSystem = {value: ''}; let defLang = {value: null}; - return await this.check(issuePath, system, version, code, abstractOk, inferSystem, null, unknownSystems, ver, inactive, normalForm, vstatus, it, op, null, null, contentMode, impliedSystem, ts, msgs, defLang); + return await this.check(issuePath, system, version, code, null, unknownSystems, ver, inactive, normalForm, vstatus, it, op, null, null, contentMode, impliedSystem, ts, msgs, defLang); } - async check(path, system, version, code, abstractOk, inferSystem, displays, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, vcc, params, contentMode, impliedSystem, unkCodes, messages, defLang) { + async check(path, system, version, code, displays, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, vcc, params, contentMode, impliedSystem, unkCodes, messages, defLang) { defLang.value = new Language('en'); this.worker.opContext.addNote(this.valueSet, 'Check "' + this.worker.renderer.displayCoded(system, version, code) + '"', this.indentCount); - if (!system && !inferSystem) { + if (!system && !this.params.inferSystem) { let msg = this.worker.i18n.translate('Coding_has_no_system__cannot_validate', this.params.HTTPLanguages, []); messages.push(msg); op.addIssue(new Issue('warning', 'invalid', path, 'Coding_has_no_system__cannot_validate', msg, 'invalid-data')); @@ -456,7 +461,7 @@ class ValueSetChecker { op.addIssue(new Issue('warning', 'invalid', path, 'Coding_has_no_system__cannot_validate_NO_INFER', msg, 'invalid-data')); return false; } - let cs = await this.worker.findCodeSystem(system, version, this.params, ['complete', 'fragment'], op,true); + let cs = await this.worker.findCodeSystem(system, version, this.params, ['complete', 'fragment'], op,true, false, false, this.worker.requiredSupplements); this.seeSourceProvider(cs, system); if (cs === null) { this.worker.opContext.addNote(this.valueSet, 'Didn\'t find CodeSystem "' + this.worker.renderer.displayCoded(system, version) + '"', this.indentCount); @@ -470,7 +475,7 @@ class ValueSetChecker { op.addIssue(new Issue('error', 'invalid', addToPath(path, 'system'), 'Terminology_TX_System_ValueSet2', msg, 'invalid-data')); unknownSystems.add(system); } else { - let css = await this.worker.findCodeSystem(system, version, this.params, ['supplement'], op,true); + let css = await this.worker.findCodeSystem(system, version, this.params, ['supplement'], op,true, false, false, this.worker.requiredSupplements); if (css !== null) { vss = null; let msg = this.worker.i18n.translate('CODESYSTEM_CS_NO_SUPPLEMENT', this.params.HTTPLanguages, [this.canonical(css.system(), css.version())]); @@ -529,7 +534,7 @@ class ValueSetChecker { vcc.addCoding(cs.system(), cs.version(), await cs.code(ctxt), cs.display(ctxt, this.params.workingLanguages())); } cause.value = 'null'; - if (!(abstractOk || !(await cs.IsAbstract(ctxt)))) { + if (!(this.params.abstractOk || !(await cs.isAbstract(ctxt)))) { result = false; this.worker.opContext.addNote(this.valueSet, 'Abstract code when not allowed', this.indentCount); cause.value = 'business-rule'; @@ -566,7 +571,7 @@ class ValueSetChecker { } } else if (DEV_IGNORE_VALUESET) { // anyhow, we ignore the value set (at least for now) - let cs = await this.worker.findCodeSystem(system, version, this.params, ['complete', 'fragment'], op, true, true, false); + let cs = await this.worker.findCodeSystem(system, version, this.params, ['complete', 'fragment'], op, true, true, false, this.worker.requiredSupplements); if (cs === null) { result = null; cause.value = 'not-found'; @@ -619,7 +624,7 @@ class ValueSetChecker { } else { ctxt = ctxt.context; cause.value = 'null'; - if (!(abstractOk || !cs.IsAbstract(ctxt))) { + if (!(this.params.abstractOk || !(await cs.isAbstract(ctxt)))) { result = false; this.worker.opContext.addNote(this.valueSet, 'Abstract code when not allowed', this.indentCount); cause.value = 'business-rule'; @@ -641,7 +646,7 @@ class ValueSetChecker { } } } else { - if (!system && inferSystem) { + if (!system && this.params.inferSystem) { let systems = new Set(); system = await this.determineSystem(this.worker.opContext, code, systems, op); if (!system) { @@ -662,8 +667,9 @@ class ValueSetChecker { } } - if (this.requiredSupplements.length > 0) { - throw new Issue('error', 'not-found', null, 'VALUESET_SUPPLEMENT_MISSING', this.worker.i18n.translatePlural(this.requiredSupplements.length, 'VALUESET_SUPPLEMENT_MISSING', this.params.HTTPLanguages, [this.requiredSupplements.join(',')])).handleAsOO(400); + const unused = new Set([...this.worker.requiredSupplements].filter(s => !this.worker.usedSupplements.has(s))); + if (unused.size > 0) { + throw new Issue('error', 'not-found', null, 'VALUESET_SUPPLEMENT_MISSING', this.worker.i18n.translatePlural(unused.size, 'VALUESET_SUPPLEMENT_MISSING', this.params.HTTPLanguages, [[...unused].join(',')]), 'not-found').handleAsOO(400); } if (Extensions.checkNoModifiers(this.valueSet.jsonObj.compose, 'ValueSetChecker.prepare', 'ValueSet.compose')) { @@ -674,7 +680,7 @@ class ValueSetChecker { result = true; } else if (cc.system === system || system === '%%null%%') { let v = await this.determineVersion(path, cc.system, cc.version, version, op, unknownSystems, messages); - let cs = await this.worker.findCodeSystem(system, v, this.params, ["complete", "fragment"], op,true, true, false); + let cs = await this.worker.findCodeSystem(system, v, this.params, ["complete", "fragment"], op,true, true, false, this.worker.requiredSupplements); if (cs === null) { this.worker.opContext.addNote(this.valueSet, 'CodeSystem not found: ' + this.worker.renderer.displayCoded(cc.system, v), this.indentCount); if (!this.params.membershipOnly) { @@ -712,11 +718,11 @@ class ValueSetChecker { this.worker.opContext.addNote(this.valueSet, 'CodeSystem found: ' + this.worker.renderer.displayCoded(cs) + ' for ' + this.worker.renderer.displayCoded(cc.system, v), this.indentCount); await this.checkCanonicalStatusCS(path, op, cs, this.valueSet); ver.value = cs.version(); - this.worker.checkSupplements(cs, cc, this.requiredSupplements); + this.worker.checkSupplements(cs, cc, this.worker.requiredSupplements, this.worker.usedSupplements); contentMode.value = cs.contentMode(); let msg = ''; - if ((system === '%%null%%' || cs.system() === system) && await this.checkConceptSet(path, 'in', cs, cc, code, abstractOk, displays, this.valueSet, msg, inactive, normalForm, vstatus, op, vcc, messages)) { + if ((system === '%%null%%' || cs.system() === system) && await this.checkConceptSet(path, 'in', cs, cc, code, displays, this.valueSet, msg, inactive, normalForm, vstatus, op, vcc, messages)) { result = true; } else { result = false; @@ -737,7 +743,7 @@ class ValueSetChecker { } this.checkCanonicalStatus(path, op, checker.valueSet, this.valueSet); if (result === true) { - result = await checker.check(path, system, version, code, abstractOk, inferSystem, displays, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, null, params, contentMode, impliedSystem, unkCodes, messages, defLang); + result = await checker.check(path, system, version, code, displays, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, null, params, contentMode, impliedSystem, unkCodes, messages, defLang); } } if (result === true) { @@ -751,21 +757,21 @@ class ValueSetChecker { if (!cc.system) { excluded = true; } else { - let cs = await this.worker.findCodeSystem(cc.system, cc.version, this.params, ['complete', 'fragment'], op,true, true, false); + let cs = await this.worker.findCodeSystem(cc.system, cc.version, this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements); if (cs === null) { throw new Issue('error', 'unknown', null, null, 'No Match for ' + cc.system + '|' + cc.version); } await this.checkCanonicalStatus(path, op, cs, this.valueSet); - this.worker.checkSupplements(cs, cc, this.requiredSupplements); + this.worker.checkSupplements(cs, cc, this.worker.requiredSupplements, this.worker.usedSupplements); ver.value = cs.version(); contentMode.value = cs.contentMode(); let msg = ''; - excluded = (system === '%%null%%' || cs.system() === system) && await this.checkConceptSet(path, 'not in', cs, cc, code, abstractOk, displays, this.valueSet, msg, inactive, normalForm, vstatus, op, vcc); + excluded = (system === '%%null%%' || cs.system() === system) && await this.checkConceptSet(path, 'not in', cs, cc, code, displays, this.valueSet, msg, inactive, normalForm, vstatus, op, vcc); if (msg) { messages.push(msg); } } - for (let u of cc.valueSets || []) { + for (let u of cc.valueSet || []) { this.worker.deadCheck('check#5'); let s = this.worker.pinValueSet(u); let checker = this.others.get(s); @@ -773,7 +779,7 @@ class ValueSetChecker { throw new Issue('error', 'unknown', null, null, 'No Match for ' + cc.system + '|' + cc.version + ' in ' + Array.from(this.others.keys()).join(',')); } this.checkCanonicalStatus(path, op, checker.valueSet, this.valueSet); - excluded = excluded && (await checker.check(path, system, version, code, abstractOk, inferSystem, displays, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, null, params, contentMode, impliedSystem, unkCodes, messages, defLang) === true); + excluded = excluded && (await checker.check(path, system, version, code, displays, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, null, params, contentMode, impliedSystem, unkCodes, messages, defLang) === true); } if (excluded) { return false; @@ -800,7 +806,7 @@ class ValueSetChecker { op.addIssueNoId('error', 'not-found', addToPath(path, 'version'), msg, 'vs-invalid'); return false; } - let cs = await this.worker.findCodeSystem(system, v, this.params, ['complete', 'fragment'], op, true, true, false); + let cs = await this.worker.findCodeSystem(system, v, this.params, ['complete', 'fragment'], op, true, true, false, this.worker.requiredSupplements); if (cs === null) { if (!this.params.membershipOnly) { let bAdd = true; @@ -838,7 +844,7 @@ class ValueSetChecker { ver.value = cs.version(); contentMode.value = cs.contentMode(); let msg = ''; - if ((system === '%%null%%' || cs.system() === system) && await this.checkExpansion(path, cs, ccc, code, abstractOk, displays, this.valueSet, msg, inactive, vstatus, op)) { + if ((system === '%%null%%' || cs.system() === system) && await this.checkExpansion(path, cs, ccc, code, displays, this.valueSet, msg, inactive, vstatus, op)) { result = true; } else { result = false; @@ -855,7 +861,7 @@ class ValueSetChecker { return result; } - async checkCoding(issuePath, coding, abstractOk, inferSystem) { + async checkCoding(issuePath, coding) { let inactive = false; let path = issuePath; let unknownSystems = new Set(); @@ -864,7 +870,7 @@ class ValueSetChecker { let result = new Parameters(); this.worker.opContext.clearContexts(); - if (inferSystem) { + if (this.params.inferSystem) { this.worker.opContext.addNote(this.valueSet, 'Validate "' + this.worker.renderer.displayCoded(coding) + '" and infer system', this.indentCount); } else { this.worker.opContext.addNote(this.valueSet, 'Validate "' + this.worker.renderer.displayCoded(coding) + '"', this.indentCount); @@ -882,7 +888,7 @@ class ValueSetChecker { let impliedSystem = {value: ''}; let defLang = {value: null}; - let ok = await this.check(path, coding.system, coding.version, coding.code, abstractOk, inferSystem, list, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, null, result, contentMode, impliedSystem, unkCodes, messages, defLang); + let ok = await this.check(path, coding.system, coding.version, coding.code, list, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, null, result, contentMode, impliedSystem, unkCodes, messages, defLang); if (ok === true) { result.AddParamBool('result', true); if ((cause.value === 'not-found' && contentMode.value !== 'complete') || contentMode.value === 'example') { @@ -963,17 +969,17 @@ class ValueSetChecker { async checkSupplementsExist(vs) { for (let inc of vs.jsonObj.compose.include) { if (inc.system) { - let cs = await this.worker.findCodeSystem(inc.system, inc.version, this.params, ['complete', 'fragment'], null,true); + let cs = await this.worker.findCodeSystem(inc.system, inc.version, this.params, ['complete', 'fragment'], null,true, false, false, this.worker.requiredSupplements); if (cs !== null) { - await this.worker.checkSupplements(cs, null, this.requiredSupplements); + await this.worker.checkSupplements(cs, null, this.worker.requiredSupplements, this.worker.usedSupplements); } } } } - async checkCodeableConcept(issuePath, code, abstractOk, inferSystem, mode) { + async checkCodeableConcept(issuePath, code, mode) { this.worker.opContext.clearContexts(); - if (inferSystem) { + if (this.params.inferSystem) { this.worker.opContext.addNote(this.valueSet, 'Validate "' + this.worker.renderer.displayCoded(code) + '" and infer system', this.indentCount); } else { this.worker.opContext.addNote(this.valueSet, 'Validate "' + this.worker.renderer.displayCoded(code) + '"', this.indentCount); @@ -1024,8 +1030,8 @@ class ValueSetChecker { mt = []; let i = 0; let impliedSystem = { value: '' }; - for (let c of code.coding) { - const csd = await this.worker.findCodeSystem(c.system, null, this.params, ['complete', 'fragment'], false, true); + for (let c of code.coding || []) { + const csd = await this.worker.findCodeSystem(c.system, null, this.params, ['complete', 'fragment'], false, true, false, false, this.worker.requiredSupplements); this.worker.seeSourceProvider(csd, c.system); this.worker.deadCheck('check-b#1'); @@ -1039,7 +1045,7 @@ class ValueSetChecker { let ver = { value: '' }; let contentMode = { value: null }; let defLang = { value: null }; - let v = await this.check(path, c.system, c.version, c.code, abstractOk, inferSystem, list, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, vcc, result, contentMode, impliedSystem, ts, mt, defLang); + let v = await this.check(path, c.system, c.version, c.code, list, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, vcc, result, contentMode, impliedSystem, ts, mt, defLang); if (v === false) { cause.value = 'code-invalid'; } @@ -1114,7 +1120,7 @@ class ValueSetChecker { mt.push(m); op.addIssue(new Issue('error', 'invalid', p, 'Terminology_TX_System_Relative', m, 'invalid-data')); } - let prov = await this.worker.findCodeSystem(ws, c.version, this.params, ['complete', 'fragment'], op,true, true, false); + let prov = await this.worker.findCodeSystem(ws, c.version, this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements); if (prov === null) { let vss = await this.worker.findValueSet(ws, ''); if (vss !== null) { @@ -1124,7 +1130,7 @@ class ValueSetChecker { op.addIssue(new Issue('error', 'invalid', addToPath(path, 'system'), 'Terminology_TX_System_ValueSet2', m, 'invalid-data')); cause.value = 'invalid'; } else { - let provS = await this.worker.findCodeSystem(ws, c.version, this.params, ['supplement'], op,true, true, false); + let provS = await this.worker.findCodeSystem(ws, c.version, this.params, ['supplement'], op,true, true, false, this.worker.requiredSupplements); if (provS !== null) { vss = null; let m = this.worker.i18n.translate('CODESYSTEM_CS_NO_SUPPLEMENT', this.params.HTTPLanguages, [provS.vurl()]); @@ -1132,7 +1138,7 @@ class ValueSetChecker { op.addIssue(new Issue('error', 'invalid', addToPath(path, 'system'), 'CODESYSTEM_CS_NO_SUPPLEMENT', m, 'invalid-data')); cause.value = 'invalid'; } else { - let prov2 = await this.worker.findCodeSystem(ws, '', this.params, ['complete', 'fragment'], op,true, true, false); + let prov2 = await this.worker.findCodeSystem(ws, '', this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements); let bAdd = true; let m, mid, vn; if (prov2 === null && !c.version) { @@ -1413,9 +1419,9 @@ class ValueSetChecker { } } - async checkSystemCode(issuePath, system, version, code, inferSystem) { + async checkSystemCode(issuePath, system, version, code) { this.worker.opContext.clearContexts(); - if (inferSystem) { + if (this.params.inferSystem) { this.worker.opContext.addNote(this.valueSet, 'Validate "' + code + '" and infer system', this.indentCount); } else { this.worker.opContext.addNote(this.valueSet, 'Validate "' + this.worker.renderer.displayCoded(system, version, code) + '"', this.indentCount); @@ -1436,7 +1442,7 @@ class ValueSetChecker { let impliedSystem = {value: ''}; let defLang = {value: null}; - let ok = await this.check(issuePath, system, version, code, true, inferSystem, list, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, null, result, contentMode, impliedSystem, unkCodes, messages, defLang); + let ok = await this.check(issuePath, system, version, code, true, list, unknownSystems, ver, inactive, normalForm, vstatus, cause, op, null, result, contentMode, impliedSystem, unkCodes, messages, defLang); if (ok === true) { result.AddParamBool('result', true); let pd = list.preferredDisplay(this.params.workingLanguages()); @@ -1485,7 +1491,7 @@ class ValueSetChecker { return result; } - async checkConceptSet(path, role, cs, cset, code, abstractOk, displays, vs, message, inactive, normalForm, vstatus, op, vcc, messages) { + async checkConceptSet(path, role, cs, cset, code, displays, vs, message, inactive, normalForm, vstatus, op, vcc, messages) { this.worker.opContext.addNote(vs, 'check code ' + role + ' ' + this.worker.renderer.displayValueSetInclude(cset) + ' at ' + path, this.indentCount); inactive.value = false; let result = false; @@ -1506,7 +1512,7 @@ class ValueSetChecker { if (loc.message && op) { op.addIssue(new Issue('information', 'code-invalid', addToPath(path, 'code'), null, loc.message, 'invalid-code')); } - } else if (!(abstractOk || !cs.IsAbstract(loc.context))) { + } else if (!(this.params.abstractOk || !(await cs.isAbstract(loc.context)))) { this.worker.opContext.addNote(this.valueSet, 'Code "' + code + '" found in ' + this.worker.renderer.displayCoded(cs) + ' but is abstract', this.indentCount); if (!this.params.membershipOnly) { op.addIssue(new Issue('error', 'business-rule', addToPath(path, 'code'), 'ABSTRACT_CODE_NOT_ALLOWED', this.worker.i18n.translate('ABSTRACT_CODE_NOT_ALLOWED', this.params.HTTPLanguages, [cs.system(), code]), 'code-rule')); @@ -1580,7 +1586,7 @@ class ValueSetChecker { this.worker.opContext.addNote(this.valueSet, 'Code "' + code + '" found in ' + this.worker.renderer.displayCoded(cs), this.indentCount); await this.worker.listDisplaysFromCodeSystem(displays, cs, loc); this.worker.listDisplaysFromIncludeConcept(displays, cc, vs); - if (!(abstractOk || !cs.IsAbstract(loc))) { + if (!(this.params.abstractOk || !(await cs.isAbstract(loc)))) { if (!this.params.membershipOnly) { op.addIssue(new Issue('error', 'business-rule', addToPath(path, 'code'), 'ABSTRACT_CODE_NOT_ALLOWED', this.worker.i18n.translate('ABSTRACT_CODE_NOT_ALLOWED', this.params.HTTPLanguages, [cs.system(), code]), 'code-rule')); } @@ -1633,7 +1639,7 @@ class ValueSetChecker { let loc = await cs.filterLocate(prep, ctxt, code); if (loc != null && !(typeof loc === 'string')) { await this.worker.listDisplaysFromCodeSystem(displays, cs, loc); - if (!(abstractOk || !cs.IsAbstract(loc))) { + if (!(this.params.abstractOk || !(await cs.isAbstract(loc)))) { this.worker.opContext.addNote(this.valueSet, 'Filter ' + ctxt.summary + ': Code "' + code + '" found in ' + this.worker.renderer.displayCoded(cs) + ' but is abstract', this.indentCount); if (!this.params.membershipOnly) { op.addIssue(new Issue('error', 'business-rule', addToPath(path, 'code'), 'ABSTRACT_CODE_NOT_ALLOWED', this.worker.i18n.translate('ABSTRACT_CODE_NOT_ALLOWED', this.params.HTTPLanguages, [cs.system(), code]), 'code-rule')); @@ -1670,7 +1676,7 @@ class ValueSetChecker { let loc = await cs.locateIsA(code, fc.value, fc.op === "descendent-of"); if (loc !== null) { await this.worker.listDisplaysFromCodeSystem(displays, cs, loc); - if (!(abstractOk || !cs.IsAbstract(loc))) { + if (!(this.params.abstractOk || !(await cs.isAbstract(loc)))) { this.worker.opContext.addNote(this.valueSet, 'Filter "' + fc.property + '' + fc.op + '' + fc.value + '": Code "' + code + '" found in ' + this.worker.renderer.displayCoded(cs) + ' but is abstract', this.indentCount); if (!this.params.membershipOnly) { op.addIssue(new Issue('error', 'business-rule', addToPath(path, 'code'), 'ABSTRACT_CODE_NOT_ALLOWED', this.worker.i18n.translate('ABSTRACT_CODE_NOT_ALLOWED', this.params.HTTPLanguages, [cs.system(), code]), 'code-rule')); @@ -1695,7 +1701,7 @@ class ValueSetChecker { loc = await cs.locate(code, null, msg); if (loc !== null) { await this.worker.listDisplaysFromCodeSystem(displays, cs, loc); - if (!(abstractOk || !cs.IsAbstract(loc))) { + if (!(this.params.abstractOk || !(await cs.isAbstract(loc)))) { this.worker.opContext.addNote(this.valueSet, 'Filter ' + fc.property + fc.op + fc.value + ': Code "' + code + '" found in ' + this.worker.renderer.displayCoded(cs) + ' but is abstract', this.indentCount); if (!this.params.membershipOnly) { op.addIssue(new Issue('error', 'business-rule', addToPath(path, 'code'), 'ABSTRACT_CODE_NOT_ALLOWED', this.worker.i18n.translate('ABSTRACT_CODE_NOT_ALLOWED', this.params.HTTPLanguages, [cs.system(), code]), 'code-rule')); @@ -1726,7 +1732,7 @@ class ValueSetChecker { let loc = await cs.filterLocate(prep, ctxt, code); if (!(typeof loc === 'string')) { await this.worker.listDisplaysFromCodeSystem(displays, cs, loc); - if (!(abstractOk || !cs.IsAbstract(loc))) { + if (!(this.params.abstractOk || !(await cs.isAbstract(loc)))) { this.worker.opContext.addNote(this.valueSet, 'Filter ' + ctxt.summary + ': Code "' + code + '" found in ' + this.worker.renderer.displayCoded(cs) + ' but is abstract', this.indentCount); if (!this.params.membershipOnly) { op.addIssue(new Issue('error', 'business-rule', addToPath(path, 'code'), 'ABSTRACT_CODE_NOT_ALLOWED', this.worker.i18n.translate('ABSTRACT_CODE_NOT_ALLOWED', this.params.HTTPLanguages, [cs.system(), code]), 'code-rule')); @@ -1761,7 +1767,7 @@ class ValueSetChecker { return result; } - async checkExpansion(path, cs, cset, code, abstractOk, displays, vs, message, inactive, vstatus, op) { + async checkExpansion(path, cs, cset, code, displays, vs, message, inactive, vstatus, op) { let result = false; let loc = await cs.locate(code, null, message); result = false; @@ -1770,7 +1776,7 @@ class ValueSetChecker { op.addIssue(new Issue('error', 'code-invalid', addToPath(path, 'code'), 'Unknown_Code_in_Version', this.worker.i18n.translate(Unknown_Code_in_VersionSCT(cs.system(), cs.version()), this.params.HTTPLanguages, [code, cs.system(), cs.version(), SCTVersion(cs.system(), cs.version())]), 'invalid-code')); } - } else if (!(abstractOk || !cs.IsAbstract(loc.context))) { + } else if (!(this.params.abstractOk || !(await cs.isAbstract(loc.context)))) { if (!this.params.membershipOnly) { op.addIssue(new Issue('error', 'business-rule', addToPath(path, 'code'), 'ABSTRACT_CODE_NOT_ALLOWED', this.worker.i18n.translate('ABSTRACT_CODE_NOT_ALLOWED', this.params.HTTPLanguages, [cs.system(), code]), 'code-rule')); } @@ -1867,6 +1873,8 @@ function toText(st, sep) { class ValidateWorker extends TerminologyWorker { + requiredSupplements = new Set(); + usedSupplements = new Set(); /** * @param {OperationContext} opContext - Operation context * @param {Logger} log - Logger instance @@ -1931,12 +1939,13 @@ class ValidateWorker extends TerminologyWorker { let txp = new TxParameters(this.languages, this.i18n, true); txp.readParams(params); + for (const item of txp.supplements) this.requiredSupplements.add(item); // Extract coded value mode = {mode: null}; coded = this.extractCodedValue(params, true, mode); if (!coded) { - throw new Issue('error', 'invalid', null, null, 'Unable to find code to validate (looked for coding | codeableConcept | code in parameters =codingX:Coding)', null, 400); + throw new Issue('error', 'invalid', null, null, 'Unable to find code to validate (looked for coding | codeableConcept | code in parameters)', null, 400).handleAsOO(400); } // Get the CodeSystem - from parameter or by url @@ -1994,6 +2003,7 @@ class ValidateWorker extends TerminologyWorker { let txp = new TxParameters(this.languages, this.i18n, true); txp.readParams(params); + for (const item of txp.supplements) this.requiredSupplements.add(item); // Get the CodeSystem by id const codeSystem = await this.provider.getCodeSystemById(this.opContext, id); @@ -2055,6 +2065,7 @@ class ValidateWorker extends TerminologyWorker { let txp = new TxParameters(this.languages, this.i18n, true); txp.readParams(params); + for (const item of txp.supplements) this.requiredSupplements.add(item); // Get the ValueSet - from parameter or by url const valueSet = await this.resolveValueSet(params, txp); @@ -2091,6 +2102,7 @@ class ValidateWorker extends TerminologyWorker { let txp = new TxParameters(this.languages, this.i18n, true); txp.readParams(params); + for (const item of txp.supplements) this.requiredSupplements.add(item); // Get the ValueSet by id const valueSet = await this.provider.getValueSetById(this.opContext, id); @@ -2126,13 +2138,13 @@ class ValidateWorker extends TerminologyWorker { * Resolve the CodeSystem to validate against * @param {Object} params - Parameters resource * @param {string|null} id - Instance id (if instance-level request) - * @returns {Object|null} CodeSystem resource (wrapper or JSON) + * @returns {CodeSystemProvider|null} CodeSystem resource (wrapper or JSON) */ async resolveCodeSystem(params, txParams, coded, mode) { // Check for codeSystem resource parameter const csResource = this.getResourceParam(params, 'codeSystem'); if (csResource) { - return csResource; + return new FhirCodeSystemProvider(this.opContext, new CodeSystem(csResource), []); // todo: supplements } let path = coded == null ? null : mode.issuePath+".system"; let fromCoded = false; @@ -2158,7 +2170,7 @@ class ValidateWorker extends TerminologyWorker { } version = this.determineVersionBase(url, version, txParams); - let supplements = this.loadSupplements(url, version); + let supplements = this.loadSupplements(url, version, this.requiredSupplements); // First check additional resources const fromAdditional = this.findInAdditionalResources(url, version, 'CodeSystem', false); @@ -2297,14 +2309,11 @@ class ValidateWorker extends TerminologyWorker { let vs = this.makeVsForCS(codeSystem); - // Get parameters - const abstractOk = this._getBoolParam(params, 'abstract', true); - // Create and prepare checker const checker = new ValueSetChecker(this, vs, params); // Perform validation - const result = await checker.checkCodeableConcept(mode.issuePath, coded, abstractOk, false, mode.mode); + const result = await checker.checkCodeableConcept(mode.issuePath, coded, mode.mode); // Add diagnostics if requested if (params.diagnostics) { @@ -2344,9 +2353,9 @@ class ValidateWorker extends TerminologyWorker { this.deadCheck('doValidationVS'); this.params = params; - // Get parameters - const abstractOk = this._getBoolParam(params, 'abstract', true); - const inferSystem = this._getBoolParam(params, 'inferSystem', false) || (mode === 'code' && !coded.coding[0].system) + for (let ext of Extensions.list(valueSet.jsonObj, 'http://hl7.org/fhir/StructureDefinition/valueset-supplement')) { + this.requiredSupplements.add(getValuePrimitive(ext)); + } // Create and prepare checker const checker = new ValueSetChecker(this, valueSet, params); @@ -2354,6 +2363,7 @@ class ValidateWorker extends TerminologyWorker { await checker.prepare(); } catch (error) { this.log.error(error); + console.log(error); if (!(error instanceof Issue) || error.isHandleAsOO()) { throw error; } else { @@ -2362,7 +2372,7 @@ class ValidateWorker extends TerminologyWorker { } // Perform validation - const result = await checker.checkCodeableConcept(issuePath, coded, abstractOk, inferSystem, mode); + const result = await checker.checkCodeableConcept(issuePath, coded, mode); // Add diagnostics if requested if (params.diagnostics) { @@ -2477,7 +2487,9 @@ class ValidateWorker extends TerminologyWorker { p.addParamResource('issues', op.jsonObj); p.addParamBool('result', false); p.addParamStr('message', error.message); - if (mode == 'codeableConcept') { + if (!mode || !coded) { + // nothing to add + } else if (mode == 'codeableConcept') { p.addParam('codeableConcept', 'valueCodeableConcept', coded); } else if (coded.coding) { if (coded.coding[0].system) { diff --git a/tx/workers/worker.js b/tx/workers/worker.js index 95402c9..ffcb3f9 100644 --- a/tx/workers/worker.js +++ b/tx/workers/worker.js @@ -133,9 +133,10 @@ class TerminologyWorker { * @param {Array} kinds - Allowed content modes * @param {OperationOutcome} op - Op for errors * * @param {boolean} nullOk - Whether null result is acceptable + * @param {Set} statedSupplements - Supplements invoked in context * @returns {CodeSystemProvider|null} Code system provider or null */ - async findCodeSystem(url, version = '', params, kinds = ['complete'], op, nullOk = false, checkVer = false, noVParams = false) { + async findCodeSystem(url, version = '', params, kinds = ['complete'], op, nullOk = false, checkVer = false, noVParams = false, statedSupplements = null) { if (!url) { return null; } @@ -145,7 +146,7 @@ class TerminologyWorker { } let codeSystemResource = null; let provider = null; - const supplements = this.loadSupplements(url, version); + const supplements = this.loadSupplements(url, version, statedSupplements); // First check additional resources codeSystemResource = this.findInAdditionalResources(url, version, 'CodeSystem', !nullOk); @@ -245,9 +246,10 @@ class TerminologyWorker { * Load supplements for a code system * @param {string} url - Code system URL * @param {string} version - Code system version + * @param {Set} statedSupplements - Supplements invoked in context * @returns {Array} Supplement code systems */ - loadSupplements(url, version = '') { + loadSupplements(url, version = '', statedSupplements) { const supplements = []; if (!this.additionalResources) { @@ -265,6 +267,10 @@ class TerminologyWorker { continue; } + // we consider either language packs or specified supplements + if (!(cs.isLangPack() || (statedSupplements && (statedSupplements.has(cs.url) || statedSupplements.has(cs.vurl))))) { + continue; + } // Handle exact URL match (no version specified in supplements) if (supplementsUrl === url) { // If we're looking for a specific version, only include if no version in supplements URL @@ -298,10 +304,10 @@ class TerminologyWorker { * @param {CodeSystemProvider} cs - Code system provider * @param {Object} src - Source element (for extensions) */ - checkSupplements(cs, src, requiredSupplements) { + checkSupplements(cs, src, requiredSupplements, usedSupplements = null) { // Check for required supplements in extensions - if (src && src.getExtensions) { - const supplementExtensions = src.getExtensions('http://hl7.org/fhir/StructureDefinition/valueset-supplement'); + if (src && src.extension) { + const supplementExtensions = src.extension.filter(x => x.url == 'http://hl7.org/fhir/StructureDefinition/valueset-supplement'); for (const ext of supplementExtensions) { const supplementUrl = ext.valueString || ext.valueUri; if (supplementUrl && !cs.hasSupplement(this.opContext, supplementUrl)) { @@ -310,10 +316,12 @@ class TerminologyWorker { } } - // Remove required supplements that are satisfied - for (let i = requiredSupplements.length - 1; i >= 0; i--) { - if (cs.hasSupplement(requiredSupplements[i])) { - requiredSupplements.splice(i, 1); + // Note required supplements that are satisfied + if (usedSupplements) { + for (const s of requiredSupplements) { + if (cs.hasSupplement(s)) { + usedSupplements.add(s); + } } } } @@ -456,6 +464,10 @@ class TerminologyWorker { if (req.method === 'POST' && req.body && req.body.resourceType === 'Parameters') { return req.body; } + if (req.method === 'POST' && req.body && req.body.resourceType) { + let langs = this.languages.parse(req.headers['accept-language']); + throw new Issue('error', 'invalid', null, 'Wrong_type_for_resource_expected', this.i18n.translate('Wrong_type_for_resource_expected', langs, ["Parameters", req.body.resourceType])).handleAsOO(400); + } // Convert query params or form body to Parameters const source = req.method === 'POST' ? {...req.query, ...req.body} : req.query; @@ -480,6 +492,10 @@ class TerminologyWorker { // Assume it's a complex type like Coding or CodeableConcept params.parameter.push(this.buildComplexParameter(name, value)); } + } else if (value == 'true') { + params.parameter.push({name, valueBoolean: true}); + } else if (value == 'false') { + params.parameter.push({name, valueBoolean: false}); } else { params.parameter.push({name, valueString: String(value)}); } @@ -837,31 +853,6 @@ class TerminologyWorker { return result; } - // Note: findParameter, getStringParam, getResourceParam, getCodingParam, - // and getCodeableConceptParam are inherited from TerminologyWorker base class - - fixForVersion(resource) { - if (this.provider.fhirVersion >= 5) { - return resource; - } - let rt = resource.resourceType; - switch (rt) { - case "ValueSet": { - let vs = new ValueSet(resource); - if (this.provider.fhirVersion == 4) { - return vs.convertFromR5(resource, "R4"); - } else if (this.provider.fhirVersion == 3) { - return vs.convertFromR5(resource, "R3"); - } else { - return resource; - } - } - default: - return resource; - } - } - - seeSourceVS(vs, url) { let s = url; if (vs) { diff --git a/tx/xversion/xv-bundle.js b/tx/xversion/xv-bundle.js new file mode 100644 index 0000000..c70f10f --- /dev/null +++ b/tx/xversion/xv-bundle.js @@ -0,0 +1,70 @@ +const {VersionUtilities} = require("../../library/version-utilities"); +const { convertResourceToR5 } = require("./xv-resource"); + +/** + * Converts input Bundle to R5 format (modifies input object for performance) + * @param {Object} jsonObj - The input Bundle object + * @param {string} version - Source FHIR version + * @returns {Object} The same object, potentially modified to R5 format + * @private + */ + +function bundleToR5(jsonObj, sourceVersion) { + if (VersionUtilities.isR5Ver(sourceVersion)) { + return jsonObj; // No conversion needed + } + + for (let be of jsonObj.entry) { + convertResourceToR5(be.resource, sourceVersion); + } + + throw new Error(`Unsupported FHIR version: ${sourceVersion}`); +} + +/** + * Converts R5 Bundle to target version format (clones object first) + * @param {Object} r5Obj - The R5 format Bundle object + * @param {string} targetVersion - Target FHIR version + * @returns {Object} New object in target version format + * @private + */ +function bundleFromR5(r5Obj, targetVersion) { + const {convertResourceFromR5} = require("./xv-resource"); + + if (VersionUtilities.isR5Ver(targetVersion)) { + return r5Obj; // No conversion needed + } + + // Clone the object to avoid modifying the original + const bundle = { + resourceType: "Bundle" + } + bundle.id = r5Obj.id; + bundle.meta = r5Obj.meta; + bundle.implicitRules = r5Obj.implicitRules; + bundle.language = r5Obj.language; + bundle.identifier = r5Obj.identifier; + bundle.type = r5Obj.type; + if (VersionUtilities.isR4Ver(targetVersion)) { + bundle.timestamp = r5Obj.timestamp; + } + bundle.total = r5Obj.total; + bundle.link = r5Obj.link; + for (let be5 of r5Obj.entry) { + let be = {}; + if (!bundle.entry) { + bundle.entry = []; + } + bundle.entry.push(be); + be.link = be5.link; + be.fullUrl = be5.fullUrl; + be.search = be5.search; + be.request = be5.request; + be.response = be5.response; + be.resource = convertResourceFromR5(be5.resource, targetVersion); + } + + return bundle; +} + +module.exports = { bundleToR5, bundleFromR5 }; \ No newline at end of file diff --git a/tx/xversion/xv-capabiliityStatement.js b/tx/xversion/xv-capabiliityStatement.js new file mode 100644 index 0000000..323f2df --- /dev/null +++ b/tx/xversion/xv-capabiliityStatement.js @@ -0,0 +1,137 @@ +const {VersionUtilities} = require("../../library/version-utilities"); + +/** + * Converts input CapabilityStatement to R5 format (modifies input object for performance) + * @param {Object} jsonObj - The input CapabilityStatement object + * @param {string} version - Source FHIR version + * @returns {Object} The same object, potentially modified to R5 format + * @private + */ + +function capabilityStatementToR5(jsonObj, sourceVersion) { + if (VersionUtilities.isR5Ver(sourceVersion)) { + return jsonObj; // No conversion needed + } + + if (VersionUtilities.isR3Ver(sourceVersion)) { + // R3: resourceType was "CapabilityStatement" (same as R4/R5) + // Convert identifier from single object to array if present + if (jsonObj.identifier && !Array.isArray(jsonObj.identifier)) { + jsonObj.identifier = [jsonObj.identifier]; + } + return jsonObj; + } + + if (VersionUtilities.isR4Ver(sourceVersion)) { + // R4 to R5: No major structural changes needed + return jsonObj; + } + throw new Error(`Unsupported FHIR version: ${sourceVersion}`); +} + +/** + * Converts R5 CapabilityStatement to target version format (clones object first) + * @param {Object} r5Obj - The R5 format CapabilityStatement object + * @param {string} targetVersion - Target FHIR version + * @returns {Object} New object in target version format + * @private + */ +function capabilityStatementFromR5(r5Obj, targetVersion) { + if (VersionUtilities.isR5Ver(targetVersion)) { + return r5Obj; // No conversion needed + } + + // Clone the object to avoid modifying the original + const cloned = JSON.parse(JSON.stringify(r5Obj)); + + if (VersionUtilities.isR4Ver(targetVersion)) { + return capabilityStatementR5ToR4(cloned); + } else if (VersionUtilities.isR3Ver(targetVersion)) { + return capabilityStatementR5ToR3(cloned); + } + + throw new Error(`Unsupported target FHIR version: ${targetVersion}`); +} + +/** + * Converts R5 CapabilityStatement to R4 format + * @param {Object} r5Obj - Cloned R5 CapabilityStatement object + * @returns {Object} R4 format CapabilityStatement + * @private + */ +function capabilityStatementR5ToR4(r5Obj) { + + // Remove R5-specific elements + if (r5Obj.versionAlgorithmString) { + delete r5Obj.versionAlgorithmString; + } + if (r5Obj.versionAlgorithmCoding) { + delete r5Obj.versionAlgorithmCoding; + } + + return r5Obj; +} + +/** + * Converts R5 CapabilityStatement to R3 format + * @param {Object} r5Obj - Cloned R5 CapabilityStatement object + * @returns {Object} R3 format CapabilityStatement + * @private + */ +function capabilityStatementR5ToR3(r5Obj) { + // First apply R4 conversions + const r4Obj = capabilityStatementR5ToR4(r5Obj); + + // Convert identifier array back to single object + if (r4Obj.identifier && Array.isArray(r4Obj.identifier)) { + if (r4Obj.identifier.length > 0) { + r4Obj.identifier = r4Obj.identifier[0]; + } else { + delete r4Obj.identifier; + } + } + + // Convert valueCanonical to valueUri throughout the object + convertCanonicalToUri(r5Obj); + + + // Convert rest.operation.definition from canonical string to Reference object + for (const rest of r4Obj.rest || []) { + for (const operation of rest.operation || []) { + if (typeof operation.definition === 'string') { + operation.definition = {reference: operation.definition}; + } + for (const resource of rest.resource || []) { + delete resource.operation; + } + } + } + + return r4Obj; +} + +function convertCanonicalToUri(obj) { + if (!obj || typeof obj !== 'object') { + return; + } + + if (Array.isArray(obj)) { + obj.forEach(item => this._convertCanonicalToUri(item)); + return; + } + + // Convert valueCanonical to valueUri + if (obj.valueCanonical !== undefined) { + obj.valueUri = obj.valueCanonical; + delete obj.valueCanonical; + } + + // Recurse into all properties + for (const key of Object.keys(obj)) { + if (typeof obj[key] === 'object') { + convertCanonicalToUri(obj[key]); + } + } +} + +module.exports = { capabilityStatementToR5, capabilityStatementFromR5 }; diff --git a/tx/xversion/xv-codesystem.js b/tx/xversion/xv-codesystem.js new file mode 100644 index 0000000..6b674ac --- /dev/null +++ b/tx/xversion/xv-codesystem.js @@ -0,0 +1,169 @@ +const {VersionUtilities} = require("../../library/version-utilities"); + +/** + * Converts input CodeSystem to R5 format (modifies input object for performance) + * @param {Object} jsonObj - The input CodeSystem object + * @param {string} version - Source FHIR version + * @returns {Object} The same object, potentially modified to R5 format + * @private + */ + +function codeSystemToR5(jsonObj, version) { + if (version === 'R5') { + return jsonObj; // Already R5, no conversion needed + } + + if (version === 'R3') { + // R3 to R5: Convert identifier from single object to array + if (jsonObj.identifier && !Array.isArray(jsonObj.identifier)) { + jsonObj.identifier = [jsonObj.identifier]; + } + return jsonObj; + } + + if (version === 'R4') { + // R4 to R5: identifier is already an array, no conversion needed + return jsonObj; + } + + throw new Error(`Unsupported FHIR version: ${version}`); +} + +/** + * Converts R5 CodeSystem to target version format (clones object first) + * @param {Object} r5Obj - The R5 format CodeSystem object + * @param {string} targetVersion - Target FHIR version + * @returns {Object} New object in target version format + * @private + */ +function codeSystemFromR5(r5Obj, targetVersion) { + if (VersionUtilities.isR5Ver(targetVersion)) { + return r5Obj; // No conversion needed + } + + // Clone the object to avoid modifying the original + const cloned = JSON.parse(JSON.stringify(r5Obj)); + + if (VersionUtilities.isR4Ver(targetVersion)) { + return codeSystemR5ToR4(cloned); + } else if (VersionUtilities.isR3Ver(targetVersion)) { + return codeSystemR5ToR3(cloned); + } + + throw new Error(`Unsupported target FHIR version: ${targetVersion}`); +} + +/** + * Converts R5 CodeSystem to R4 format + * @param {Object} r5Obj - Cloned R5 CodeSystem object + * @returns {Object} R4 format CodeSystem + * @private + */ +function codeSystemR5ToR4(r5Obj) { + // Remove R5-specific elements that don't exist in R4 + if (r5Obj.versionAlgorithmString) { + delete r5Obj.versionAlgorithmString; + } + if (r5Obj.versionAlgorithmCoding) { + delete r5Obj.versionAlgorithmCoding; + } + + // Filter out R5-only filter operators + if (r5Obj.filter && Array.isArray(r5Obj.filter)) { + r5Obj.filter = r5Obj.filter.map(filter => { + if (filter.operator && Array.isArray(filter.operator)) { + // Remove R5-only operators like 'generalizes' + filter.operator = filter.operator.filter(op => + !isR5OnlyFilterOperator(op) + ); + } + return filter; + }).filter(filter => + // Remove filters that have no valid operators left + !filter.operator || filter.operator.length > 0 + ); + } + + return r5Obj; +} + +/** + * Converts R5 CodeSystem to R3 format + * @param {Object} r5Obj - Cloned R5 CodeSystem object + * @returns {Object} R3 format CodeSystem + * @private + */ +function codeSystemR5ToR3(r5Obj) { + // First apply R4 conversions + const r4Obj = codeSystemR5ToR4(r5Obj); + + // R5/R4 to R3: Convert identifier from array back to single object + if (r4Obj.identifier && Array.isArray(r4Obj.identifier)) { + if (r4Obj.identifier.length > 0) { + // Take the first identifier if multiple exist + r4Obj.identifier = r4Obj.identifier[0]; + } else { + // Remove empty array + delete r4Obj.identifier; + } + } + + // Remove additional R4-specific elements that don't exist in R3 + if (r4Obj.supplements) { + delete r4Obj.supplements; + } + + // R3 has more limited filter operator support + if (r4Obj.filter && Array.isArray(r4Obj.filter)) { + r4Obj.filter = r4Obj.filter.map(filter => { + if (filter.operator && Array.isArray(filter.operator)) { + // Keep only R3-compatible operators + filter.operator = filter.operator.filter(op => + isR3CompatibleFilterOperator(op) + ); + } + return filter; + }).filter(filter => + // Remove filters that have no valid operators left + !filter.operator || filter.operator.length > 0 + ); + } + + return r4Obj; +} + +/** + * Checks if a filter operator is R5-only + * @param {string} operator - Filter operator code + * @returns {boolean} True if operator is R5-only + * @private + */ +function isR5OnlyFilterOperator(operator) { + const r5OnlyOperators = [ + 'generalizes', // Added in R5 + // Add other R5-only operators as they're identified + ]; + return r5OnlyOperators.includes(operator); +} + +/** + * Checks if a filter operator is compatible with R3 + * @param {string} operator - Filter operator code + * @returns {boolean} True if operator is R3-compatible + * @private + */ +function isR3CompatibleFilterOperator(operator) { + const r3CompatibleOperators = [ + '=', // Equal + 'is-a', // Is-A relationship + 'descendent-of', // Descendant of (note: R3 spelling) + 'is-not-a', // Is-Not-A relationship + 'regex', // Regular expression + 'in', // In set + 'not-in', // Not in set + 'exists', // Property exists + ]; + return r3CompatibleOperators.includes(operator); +} + +module.exports = { codeSystemToR5, codeSystemFromR5 }; \ No newline at end of file diff --git a/tx/xversion/xv-conceptmap.js b/tx/xversion/xv-conceptmap.js new file mode 100644 index 0000000..d38eeb6 --- /dev/null +++ b/tx/xversion/xv-conceptmap.js @@ -0,0 +1,224 @@ +const {VersionUtilities} = require("../../library/version-utilities"); + +/** + * Converts input ConceptMap to R5 format (modifies input object for performance) + * @param {Object} jsonObj - The input ConceptMap object + * @param {string} version - Source FHIR version + * @returns {Object} The same object, potentially modified to R5 format + * @private + */ + +function conceptMapToR5(jsonObj, sourceVersion) { + if (VersionUtilities.isR5Ver(sourceVersion)) { + return jsonObj; // No conversion needed + } + + if (VersionUtilities.isR3Ver(sourceVersion) || VersionUtilities.isR4Ver(sourceVersion)) { + // Convert identifier from single object to array + if (jsonObj.identifier && !Array.isArray(jsonObj.identifier)) { + jsonObj.identifier = [jsonObj.identifier]; + } + + // Convert source/target to sourceScope/targetScope + if (jsonObj.source !== undefined) { + // Combine source + sourceVersion if both exist + if (jsonObj.sourceVersion) { + jsonObj.sourceScope = `${jsonObj.source}|${jsonObj.sourceVersion}`; + delete jsonObj.sourceVersion; + } else { + jsonObj.sourceScope = jsonObj.source; + } + delete jsonObj.source; + } + + if (jsonObj.target !== undefined) { + // Combine target + targetVersion if both exist + if (jsonObj.targetVersion) { + jsonObj.targetScope = `${jsonObj.target}|${jsonObj.targetVersion}`; + delete jsonObj.targetVersion; + } else { + jsonObj.targetScope = jsonObj.target; + } + delete jsonObj.target; + } + + // Convert equivalence to relationship in group.element.target + if (jsonObj.group && Array.isArray(jsonObj.group)) { + jsonObj.group.forEach(group => { + if (group.element && Array.isArray(group.element)) { + group.element.forEach(element => { + if (element.target && Array.isArray(element.target)) { + element.target.forEach(target => { + if (target.equivalence && !target.relationship) { + // Convert equivalence to relationship and keep both + target.relationship = convertEquivalenceToRelationship(target.equivalence); + // Keep equivalence for backward compatibility + } + }); + } + }); + } + }); + } + + return jsonObj; + } + throw new Error(`Unsupported FHIR version: ${sourceVersion}`); +} + +/** + * Converts R5 ConceptMap to target version format (clones object first) + * @param {Object} r5Obj - The R5 format ConceptMap object + * @param {string} targetVersion - Target FHIR version + * @returns {Object} New object in target version format + * @private + */ +function conceptMapFromR5(r5Obj, targetVersion) { + if (VersionUtilities.isR5Ver(targetVersion)) { + return r5Obj; // No conversion needed + } + + // Clone the object to avoid modifying the original + const cloned = JSON.parse(JSON.stringify(r5Obj)); + + if (VersionUtilities.isR4Ver(targetVersion)) { + return conceptMapR5ToR4(cloned); + } else if (VersionUtilities.isR3Ver(targetVersion)) { + return conceptMapR5ToR3(cloned); + } + + throw new Error(`Unsupported target FHIR version: ${targetVersion}`); +} + +/** + * Converts R5 ConceptMap to R4 format + * @param {Object} r5Obj - Cloned R5 ConceptMap object + * @returns {Object} R4 format ConceptMap + * @private + */ +function conceptMapR5ToR4(r5Obj) { + // Remove R5-specific elements + if (r5Obj.versionAlgorithmString) { + delete r5Obj.versionAlgorithmString; + } + if (r5Obj.versionAlgorithmCoding) { + delete r5Obj.versionAlgorithmCoding; + } + if (r5Obj.property) { + delete r5Obj.property; + } + if (r5Obj.additionalAttribute) { + delete r5Obj.additionalAttribute; + } + + // Convert identifier array back to single object + if (r5Obj.identifier && Array.isArray(r5Obj.identifier)) { + if (r5Obj.identifier.length > 0) { + r5Obj.identifier = r5Obj.identifier[0]; // Take first identifier + } else { + delete r5Obj.identifier; + } + } + + // Convert sourceScope/targetScope back to source/target + version + if (r5Obj.sourceScope) { + const parts = r5Obj.sourceScope.split('|'); + r5Obj.source = parts[0]; + if (parts.length > 1) { + r5Obj.sourceVersion = parts[1]; + } + delete r5Obj.sourceScope; + } + + if (r5Obj.targetScope) { + const parts = r5Obj.targetScope.split('|'); + r5Obj.target = parts[0]; + if (parts.length > 1) { + r5Obj.targetVersion = parts[1]; + } + delete r5Obj.targetScope; + } + + // Convert relationship back to equivalence in group.element.target + if (r5Obj.group && Array.isArray(r5Obj.group)) { + r5Obj.group.forEach(group => { + if (group.element && Array.isArray(group.element)) { + group.element.forEach(element => { + if (element.target && Array.isArray(element.target)) { + element.target.forEach(target => { + // If we have both equivalence and relationship, prefer equivalence for R4 + if (target.relationship && !target.equivalence) { + target.equivalence = convertRelationshipToEquivalence(target.relationship); + } + // Remove R5-only relationship field + delete target.relationship; + }); + } + }); + } + }); + } + + return r5Obj; +} + +/** + * Converts R5 ConceptMap to R3 format + * @param {Object} r5Obj - Cloned R5 ConceptMap object + * @returns {Object} R3 format ConceptMap + * @private + */ +function conceptMapR5ToR3(r5Obj) { + // First apply R4 conversions + const r4Obj = conceptMapR5ToR4(r5Obj); + + return r4Obj; +} + + +/** + * Converts R3/R4 equivalence to R5 relationship + * @param {string} equivalence - R3/R4 equivalence value + * @returns {string} R5 relationship value + * @private + */ +function convertEquivalenceToRelationship(equivalence) { + const equivalenceToRelationship = { + 'relatedto': 'related-to', + 'equivalent': 'equivalent', + 'equal': 'equivalent', + 'wider': 'source-is-broader-than-target', + 'subsumes': 'source-is-broader-than-target', + 'narrower': 'source-is-narrower-than-target', + 'specializes': 'source-is-narrower-than-target', + 'inexact': 'not-related-to', + 'unmatched': 'not-related-to', + 'disjoint': 'not-related-to' + }; + return equivalenceToRelationship[equivalence] || 'related-to'; +} + +/** + * Converts R5 relationship back to R3/R4 equivalence + * @param {string} relationship - R5 relationship value + * @returns {string} R3/R4 equivalence value + * @private + */ +function convertRelationshipToEquivalence(relationship) { + const relationshipToEquivalence = { + 'related-to': 'relatedto', + 'equivalent': 'equivalent', + 'source-is-broader-than-target': 'wider', + 'source-is-narrower-than-target': 'narrower', + 'not-related-to': 'unmatched' + }; + return relationshipToEquivalence[relationship] || 'relatedto'; +} + + + +module.exports = { conceptMapToR5, conceptMapFromR5 }; + + + + diff --git a/tx/xversion/xv-namingsystem.js b/tx/xversion/xv-namingsystem.js new file mode 100644 index 0000000..279994a --- /dev/null +++ b/tx/xversion/xv-namingsystem.js @@ -0,0 +1,88 @@ +const {VersionUtilities} = require("../../library/version-utilities"); + +/** + * Converts input NamingSystem to R5 format (modifies input object for performance) + * @param {Object} jsonObj - The input NamingSystem object + * @param {string} version - Source FHIR version + * @returns {Object} The same object, potentially modified to R5 format + * @private + */ + +function namingSystemToR5(jsonObj, sourceVersion) { + if (VersionUtilities.isR5Ver(sourceVersion)) { + return jsonObj; // No conversion needed + } + if (VersionUtilities.isR3Ver(sourceVersion)) { + // R3 to R5: Remove replacedBy field (we ignore it completely) + if (jsonObj.replacedBy !== undefined) { + delete jsonObj.replacedBy; + } + return jsonObj; + } + + if (VersionUtilities.isR4Ver(sourceVersion)) { + // R4 to R5: No structural conversion needed + // R5 is backward compatible for the structural elements we care about + return jsonObj; + } + return jsonObj; +} + +/** + * Converts R5 NamingSystem to target version format (clones object first) + * @param {Object} r5Obj - The R5 format NamingSystem object + * @param {string} targetVersion - Target FHIR version + * @returns {Object} New object in target version format + * @private + */ +function namingSystemFromR5(r5Obj, targetVersion) { + if (VersionUtilities.isR5Ver(targetVersion)) { + return r5Obj; // No conversion needed + } + + // Clone the object to avoid modifying the original + const cloned = JSON.parse(JSON.stringify(r5Obj)); + + if (VersionUtilities.isR4Ver(targetVersion)) { + return namingSystemR5ToR4(cloned); + } else if (VersionUtilities.isR3Ver(targetVersion)) { + return namingSystemR5ToR3(cloned); + } + + throw new Error(`Unsupported target FHIR version: ${targetVersion}`); +} + +/** + * Converts R5 NamingSystem to R4 format + * @param {Object} r5Obj - Cloned R5 NamingSystem object + * @returns {Object} R4 format NamingSystem + * @private + */ +function namingSystemR5ToR4(r5Obj) { + if (r5Obj.versionAlgorithmString) { + delete r5Obj.versionAlgorithmString; + } + if (r5Obj.versionAlgorithmCoding) { + delete r5Obj.versionAlgorithmCoding; + } + + return r5Obj; +} + +/** + * Converts R5 NamingSystem to R3 format + * @param {Object} r5Obj - Cloned R5 NamingSystem object + * @returns {Object} R3 format NamingSystem + * @private + */ +function namingSystemR5ToR3(r5Obj) { + // First apply R4 conversions + const r4Obj = namingSystemR5ToR4(r5Obj); + + // R3 doesn't have some R4/R5 fields, but we'll just let them through + // since most additions are backward compatible in JSON + return r4Obj; +} + +module.exports = { namingSystemToR5, namingSystemFromR5 }; + diff --git a/tx/xversion/xv-operationoutcome.js b/tx/xversion/xv-operationoutcome.js new file mode 100644 index 0000000..fc5bfc4 --- /dev/null +++ b/tx/xversion/xv-operationoutcome.js @@ -0,0 +1,27 @@ +/** + * Converts input OperationOutcome to R5 format (modifies input object for performance) + * @param {Object} jsonObj - The input OperationOutcome object + * @param {string} version - Source FHIR version + * @returns {Object} The same object, potentially modified to R5 format + * @private + */ + +// eslint-disable-next-line no-unused-vars +function operationOutcomeToR5(jsonObj, sourceVersion) { + return jsonObj; // No conversion needed +} + +/** + * Converts R5 OperationOutcome to target version format (clones object first) + * @param {Object} r5Obj - The R5 format OperationOutcome object + * @param {string} targetVersion - Target FHIR version + * @returns {Object} New object in target version format + * @private + */ +// eslint-disable-next-line no-unused-vars +function operationOutcomeFromR5(r5Obj, targetVersion) { + return r5Obj; // No conversion needed +} + + +module.exports = { operationOutcomeToR5, operationOutcomeFromR5 }; \ No newline at end of file diff --git a/tx/xversion/xv-parameters.js b/tx/xversion/xv-parameters.js new file mode 100644 index 0000000..a723d6f --- /dev/null +++ b/tx/xversion/xv-parameters.js @@ -0,0 +1,87 @@ +const {VersionUtilities} = require("../../library/version-utilities"); + +/** + * Converts input Parameters to R5 format (modifies input object for performance) + * @param {Object} jsonObj - The input Parameters object + * @param {string} version - Source FHIR version + * @returns {Object} The same object, potentially modified to R5 format + * @private + */ + +function parametersToR5(jsonObj, sourceVersion) { + if (VersionUtilities.isR5Ver(sourceVersion)) { + return jsonObj; // No conversion needed + } + throw new Error(`Unsupported FHIR version: ${sourceVersion}`); +} + +/** + * Converts R5 Parameters to target version format (clones object first) + * @param {Object} r5Obj - The R5 format Parameters object + * @param {string} targetVersion - Target FHIR version + * @returns {Object} New object in target version format + * @private + */ +function parametersFromR5(r5Obj, targetVersion) { + if (VersionUtilities.isR5Ver(targetVersion)) { + return r5Obj; // No conversion needed + } + + // Clone the object to avoid modifying the original + const cloned = JSON.parse(JSON.stringify(r5Obj)); + + if (VersionUtilities.isR4Ver(targetVersion)) { + return parametersR5ToR4(cloned); + } else if (VersionUtilities.isR3Ver(targetVersion)) { + return parametersR5ToR3(cloned); + } + + throw new Error(`Unsupported target FHIR version: ${targetVersion}`); +} + +/** + * Converts R5 Parameters to R4 format + * @param {Object} r5Obj - Cloned R5 Parameters object + * @returns {Object} R4 format Parameters + * @private + */ +function parametersR5ToR4(r5Obj) { + const {convertResourceFromR5} = require("./xv-resource"); + + for (let p of r5Obj.parameter) { + if (p.resource) { + p.resource = convertResourceFromR5(p.resource, "R4"); + } + } + return r5Obj; +} + +function convertParameterR5ToR3(p) { + if (p.valueCanonical) { + p.valueUri = p.valueCanonical; + delete p.valueCanonical; + } + for (const pp of p.part || []) { + convertParameterR5ToR3(pp) + } +} + +/** + * Converts R5 Parameters to R3 format + * @param {Object} r5Obj - Cloned R5 Parameters object + * @returns {Object} R3 format Parameters + * @private + */ +function parametersR5ToR3(r5Obj) { + const {convertResourceFromR5} = require("./xv-resource"); + + for (let p of r5Obj.parameter) { + if (p.resource) { + p.resource = convertResourceFromR5(p.resource, "R3"); + } + convertParameterR5ToR3(p); + } + return r5Obj; +} + +module.exports = { parametersToR5, parametersFromR5 }; \ No newline at end of file diff --git a/tx/xversion/xv-resource.js b/tx/xversion/xv-resource.js new file mode 100644 index 0000000..4930646 --- /dev/null +++ b/tx/xversion/xv-resource.js @@ -0,0 +1,45 @@ +const {codeSystemFromR5} = require("./xv-codesystem"); +const {capabilityStatementFromR5} = require("./xv-capabiliityStatement"); +const {terminologyCapabilitiesFromR5} = require("./xv-terminologyCapabilities"); +const {valueSetFromR5} = require("./xv-valueset"); +const {conceptMapFromR5} = require("./xv-conceptmap"); +const {parametersFromR5} = require("./xv-parameters"); +const {operationOutcomeFromR5} = require("./xv-operationoutcome"); +const {bundleFromR5} = require("./xv-bundle"); + + +function convertResourceToR5(data, sourceVersion) { + if (sourceVersion == "5.0" || !data.resourceType) { + return data; + } + switch (data.resourceType) { + case "CodeSystem": return codeSystemFromR5(data, sourceVersion); + case "CapabilityStatement": return capabilityStatementFromR5(data, sourceVersion); + case "TerminologyCapabilities": return terminologyCapabilitiesFromR5(data, sourceVersion); + case "ValueSet": return valueSetFromR5(data, sourceVersion); + case "ConceptMap": return conceptMapFromR5(data, sourceVersion); + case "Parameters": return parametersFromR5(data, sourceVersion); + case "OperationOutcome": return operationOutcomeFromR5(data, sourceVersion); + case "Bundle": return bundleFromR5(data, sourceVersion); + default: return data; + } +} + +function convertResourceFromR5(data, targetVersion) { + if (targetVersion == "5.0" || !data.resourceType) { + return data; + } + switch (data.resourceType) { + case "CodeSystem": return codeSystemFromR5(data, targetVersion); + case "CapabilityStatement": return capabilityStatementFromR5(data, targetVersion); + case "TerminologyCapabilities": return terminologyCapabilitiesFromR5(data, targetVersion); + case "ValueSet": return valueSetFromR5(data, targetVersion); + case "ConceptMap": return conceptMapFromR5(data, targetVersion); + case "Parameters": return parametersFromR5(data, targetVersion); + case "OperationOutcome": return operationOutcomeFromR5(data, targetVersion); + case "Bundle": return bundleFromR5(data, targetVersion); + default: return data; + } +} + +module.exports = { convertResourceToR5, convertResourceFromR5 }; \ No newline at end of file diff --git a/tx/xversion/xv-terminologyCapabilities.js b/tx/xversion/xv-terminologyCapabilities.js new file mode 100644 index 0000000..c6da2c9 --- /dev/null +++ b/tx/xversion/xv-terminologyCapabilities.js @@ -0,0 +1,214 @@ +const {VersionUtilities} = require("../../library/version-utilities"); + +/** + * Converts input TerminologyCapabilities to R5 format (modifies input object for performance) + * @param {Object} jsonObj - The input TerminologyCapabilities object + * @param {string} version - Source FHIR version + * @returns {Object} The same object, potentially modified to R5 format + * @private + */ + +function terminologyCapabilitiesToR5(jsonObj, sourceVersion) { + if (VersionUtilities.isR5Ver(sourceVersion)) { + return jsonObj; // No conversion needed + } + + if (VersionUtilities.isR4Ver(sourceVersion)) { + // R4 to R5: No major structural changes needed for TerminologyCapabilities + return jsonObj; + } + + if (VersionUtilities.isR3Ver(sourceVersion)) { + // R3: TerminologyCapabilities doesn't exist - it's a Parameters resource + // Convert from Parameters format to TerminologyCapabilities + return convertParametersToR5(jsonObj); + } + + return jsonObj; +} + + +/** + * Converts R3 Parameters format to R5 TerminologyCapabilities + * @param {Object} params - The Parameters resource + * @returns {Object} TerminologyCapabilities in R5 format + * @private + */ +function convertParametersToR5(params) { + if (params.resourceType !== 'Parameters') { + throw new Error('R3 TerminologyCapabilities must be a Parameters resource'); + } + + const result = { + resourceType: 'TerminologyCapabilities', + id: params.id, + status: 'active', // Default, as Parameters doesn't carry this + kind: 'instance', // Default for terminology server capabilities + codeSystem: [] + }; + + const parameters = params.parameter || []; + let currentSystem = null; + + for (const param of parameters) { + switch (param.name) { + case 'url': + result.url = param.valueUri; + break; + case 'version': + if (currentSystem) { + // This is a code system version + if (param.valueCode) { + currentSystem.version = currentSystem.version || []; + currentSystem.version.push({ code: param.valueCode }); + } + // Empty version parameter means no specific version + } else { + // This is the TerminologyCapabilities version + result.version = param.valueCode || param.valueString; + } + break; + case 'date': + result.date = param.valueDateTime; + break; + case 'system': + // Start a new code system + currentSystem = { uri: param.valueUri }; + result.codeSystem.push(currentSystem); + break; + case 'expansion.parameter': + result.expansion = result.expansion || { parameter: [] }; + result.expansion.parameter.push({ name: param.valueCode }); + break; + } + } + + return result; +} + + +/** + * Converts R5 TerminologyCapabilities to target version format (clones object first) + * @param {Object} r5Obj - The R5 format TerminologyCapabilities object + * @param {string} targetVersion - Target FHIR version + * @returns {Object} New object in target version format + * @private + */ +function terminologyCapabilitiesFromR5(r5Obj, targetVersion) { + if (VersionUtilities.isR5Ver(targetVersion)) { + return r5Obj; // No conversion needed + } + + // Clone the object to avoid modifying the original + const cloned = JSON.parse(JSON.stringify(r5Obj)); + + if (VersionUtilities.isR4Ver(targetVersion)) { + return terminologyCapabilitiesR5ToR4(cloned); + } else if (VersionUtilities.isR3Ver(targetVersion)) { + return terminologyCapabilitiesR5ToR3(cloned); + } + + throw new Error(`Unsupported target FHIR version: ${targetVersion}`); +} + +/** + * Converts R5 TerminologyCapabilities to R4 format + * @param {Object} r5Obj - Cloned R5 TerminologyCapabilities object + * @returns {Object} R4 format TerminologyCapabilities + * @private + */ +function terminologyCapabilitiesR5ToR4(r5Obj) { + + if (r5Obj.versionAlgorithmString) { + delete r5Obj.versionAlgorithmString; + } + if (r5Obj.versionAlgorithmCoding) { + delete r5Obj.versionAlgorithmCoding; + } + + return r5Obj; +} + +/** + * Converts R5 TerminologyCapabilities to R3 format + * @param {Object} r5Obj - Cloned R5 TerminologyCapabilities object + * @returns {Object} R3 format TerminologyCapabilities + * @private + */ +function terminologyCapabilitiesR5ToR3(r5Obj) { + // In R3, TerminologyCapabilities didn't exist - we represent it as a Parameters resource + const params = { + resourceType: 'Parameters', + id: r5Obj.id, + parameter: [] + }; + + // Add url parameter + if (r5Obj.url) { + params.parameter.push({ + name: 'url', + valueUri: r5Obj.url + }); + } + + // Add version parameter + if (r5Obj.version) { + params.parameter.push({ + name: 'version', + valueCode: r5Obj.version + }); + } + + // Add date parameter + if (r5Obj.date) { + params.parameter.push({ + name: 'date', + valueDateTime: r5Obj.date + }); + } + + // Add code systems with their versions + for (const codeSystem of r5Obj.codeSystem || []) { + // Add system parameter + params.parameter.push({ + name: 'system', + valueUri: codeSystem.uri + }); + + // Add version parameter(s) for this code system + if (codeSystem.version && codeSystem.version.length > 0) { + for (const ver of codeSystem.version) { + if (ver.code) { + params.parameter.push({ + name: 'version', + valueCode: ver.code + }); + } else { + // Empty version parameter when no specific version + params.parameter.push({ + name: 'version' + }); + } + } + } else { + // No version specified for this code system + params.parameter.push({ + name: 'version' + }); + } + } + + // Add expansion parameters + if (r5Obj.expansion && r5Obj.expansion.parameter) { + for (const expParam of r5Obj.expansion.parameter) { + params.parameter.push({ + name: 'expansion.parameter', + valueCode: expParam.name + }); + } + } + + return params; +} + +module.exports = { terminologyCapabilitiesToR5, terminologyCapabilitiesFromR5 }; diff --git a/tx/xversion/xv-valueset.js b/tx/xversion/xv-valueset.js new file mode 100644 index 0000000..ff06d2f --- /dev/null +++ b/tx/xversion/xv-valueset.js @@ -0,0 +1,234 @@ +const {VersionUtilities} = require("../../library/version-utilities"); +const {getValueName} = require("../../library/utilities"); + +/** + * Converts input ValueSet to R5 format (modifies input object for performance) + * @param {Object} jsonObj - The input ValueSet object + * @param {string} version - Source FHIR version + * @returns {Object} The same object, potentially modified to R5 format + * @private + */ + +function valueSetToR5(jsonObj, sourceVersion) { + if (VersionUtilities.isR5Ver(sourceVersion)) { + return jsonObj; // No conversion needed + } + if (VersionUtilities.isR4Ver(sourceVersion)) { + return jsonObj; // No conversion needed + } + if (VersionUtilities.isR3Ver(sourceVersion)) { + // R3 to R5: Remove extensible field (we ignore it completely) + if (jsonObj.extensible !== undefined) { + delete jsonObj.extensible; + } + return jsonObj; // No conversion needed + } + throw new Error(`Unsupported FHIR version: ${sourceVersion}`); +} + +/** + * Converts R5 ValueSet to target version format (clones object first) + * @param {Object} r5Obj - The R5 format ValueSet object + * @param {string} targetVersion - Target FHIR version + * @returns {Object} New object in target version format + * @private + */ +function valueSetFromR5(r5Obj, targetVersion) { + if (VersionUtilities.isR5Ver(targetVersion)) { + return r5Obj; // No conversion needed + } + + // Clone the object to avoid modifying the original + const cloned = JSON.parse(JSON.stringify(r5Obj)); + + if (VersionUtilities.isR4Ver(targetVersion)) { + return valueSetR5ToR4(cloned); + } else if (VersionUtilities.isR3Ver(targetVersion)) { + return valueSetR5ToR3(cloned); + } + + throw new Error(`Unsupported target FHIR version: ${targetVersion}`); +} + +/** + * Converts R5 ValueSet to R4 format + * @param {Object} r5Obj - Cloned R5 ValueSet object + * @returns {Object} R4 format ValueSet + * @private + */ +function valueSetR5ToR4(r5Obj) { + if (r5Obj.versionAlgorithmString) { + delete r5Obj.versionAlgorithmString; + } + if (r5Obj.versionAlgorithmCoding) { + delete r5Obj.versionAlgorithmCoding; + } + + // Filter out R5-only filter operators in compose + if (r5Obj.compose && r5Obj.compose.include) { + r5Obj.compose.include = r5Obj.compose.include.map(include => { + if (include.filter && Array.isArray(include.filter)) { + include.filter = include.filter.map(filter => { + if (filter.op && isR5OnlyFilterOperator(filter.op)) { + // Remove R5-only operators + return null; + } + return filter; + }).filter(filter => filter !== null); + } + return include; + }); + } + + if (r5Obj.compose && r5Obj.compose.exclude) { + r5Obj.compose.exclude = r5Obj.compose.exclude.map(exclude => { + if (exclude.filter && Array.isArray(exclude.filter)) { + exclude.filter = exclude.filter.map(filter => { + if (filter.op && isR5OnlyFilterOperator(filter.op)) { + // Remove R5-only operators + return null; + } + return filter; + }).filter(filter => filter !== null); + } + return exclude; + }); + } + + if (r5Obj.expansion) { + let exp = r5Obj.expansion; + + // Convert ValueSet.expansion.property to extensions + if (exp.property && exp.property.length > 0) { + exp.extension = exp.extension || []; + for (let prop of exp.property) { + exp.extension.push({ + url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.property", + extension: [ + { url: "code", valueCode: prop.code }, + { url: "uri", valueUri: prop.uri } + ] + }); + } + delete exp.property; + convertContainsPropertyR5ToR4(exp.contains); + + } + } + + return r5Obj; +} + +/** + * Converts R5 ValueSet to R3 format + * @param {Object} r5Obj - Cloned R5 ValueSet object + * @returns {Object} R3 format ValueSet + * @private + */ +function valueSetR5ToR3(r5Obj) { + // First apply R4 conversions + const r4Obj = valueSetR5ToR4(r5Obj); + + // R3 has more limited filter operator support + if (r4Obj.compose && r4Obj.compose.include) { + r4Obj.compose.include = r4Obj.compose.include.map(include => { + if (include.filter && Array.isArray(include.filter)) { + include.filter = include.filter.map(filter => { + if (filter.op && !isR3CompatibleFilterOperator(filter.op)) { + // Remove non-R3-compatible operators + return null; + } + return filter; + }).filter(filter => filter !== null); + } + return include; + }); + } + + if (r4Obj.compose && r4Obj.compose.exclude) { + r4Obj.compose.exclude = r4Obj.compose.exclude.map(exclude => { + if (exclude.filter && Array.isArray(exclude.filter)) { + exclude.filter = exclude.filter.map(filter => { + if (filter.op && !isR3CompatibleFilterOperator(filter.op)) { + // Remove non-R3-compatible operators + return null; + } + return filter; + }).filter(filter => filter !== null); + } + return exclude; + }); + } + return r4Obj; +} + + + +// Recursive function to convert contains.property +function convertContainsPropertyR5ToR4(containsList) { + if (!containsList) return; + + for (let item of containsList) { + if (item.property && item.property.length > 0) { + item.extension = item.extension || []; + for (let prop of item.property) { + let ext = { + url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.contains.property", + extension: [ + { url: "code", valueCode: prop.code } + ] + }; + let pn = getValueName(prop); + let subExt = { url: "value" }; + subExt[pn] = prop[pn]; + ext.extension.push(subExt); + item.extension.push(ext); + } + delete item.property; + } + + // Recurse into nested contains + if (item.contains) { + convertContainsPropertyR5ToR4(item.contains); + } + } +} + + +/** + * Checks if a filter operator is R5-only + * @param {string} operator - Filter operator code + * @returns {boolean} True if operator is R5-only + * @private + */ +function isR5OnlyFilterOperator(operator) { + const r5OnlyOperators = [ + 'generalizes', // Added in R5 + // Add other R5-only operators as they're identified + ]; + return r5OnlyOperators.includes(operator); +} + +/** + * Checks if a filter operator is compatible with R3 + * @param {string} operator - Filter operator code + * @returns {boolean} True if operator is R3-compatible + * @private + */ +function isR3CompatibleFilterOperator(operator) { + const r3CompatibleOperators = [ + '=', // Equal + 'is-a', // Is-A relationship + 'descendent-of', // Descendant of (note: R3 spelling) + 'is-not-a', // Is-Not-A relationship + 'regex', // Regular expression + 'in', // In set + 'not-in', // Not in set + 'exists', // Property exists + ]; + return r3CompatibleOperators.includes(operator); +} + + +module.exports = { valueSetToR5, valueSetFromR5 }; + diff --git a/utilities/explode-results.js b/utilities/explode-results.js new file mode 100644 index 0000000..68ce738 --- /dev/null +++ b/utilities/explode-results.js @@ -0,0 +1,58 @@ +import { readdirSync, readFileSync, mkdirSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +const dir = '/Users/grahamegrieve/temp/tx-comp/'; + +function stripDiagnostics(jsonStr) { + let obj; + try { obj = JSON.parse(jsonStr); } catch { return null; } + if (obj.resourceType === 'Parameters' && Array.isArray(obj.parameter)) { + obj.parameter = obj.parameter.filter(p => p.name !== 'diagnostics'); + } + return obj; +} + +const files = readdirSync(dir).filter(f => f.startsWith('system-') && f.endsWith('.ndjson')); + +let totalWritten = 0; +let totalSkipped = 0; + +for (const file of files) { + const subdir = join(dir, file.replace('.ndjson', '')); + let prodDirCreated = false; + let devDirCreated = false; + const prodDir = join(subdir, 'prod'); + const devDir = join(subdir, 'dev'); + + const lines = readFileSync(join(dir, file), 'utf8').split('\n').filter(l => l.trim()); + let written = 0; + let skipped = 0; + for (const line of lines) { + let obj; + try { obj = JSON.parse(line); } catch { continue; } + const id = obj.id || `unknown-${written + skipped}`; + + const prod = obj.prodBody ? stripDiagnostics(obj.prodBody) : null; + const dev = obj.devBody ? stripDiagnostics(obj.devBody) : null; + + // Compare after stripping diagnostics + const prodStr = prod ? JSON.stringify(prod) : ''; + const devStr = dev ? JSON.stringify(dev) : ''; + if (prodStr === devStr) { + skipped++; + continue; + } + + // They differ - write them out + if (!prodDirCreated) { mkdirSync(prodDir, { recursive: true }); prodDirCreated = true; } + if (!devDirCreated) { mkdirSync(devDir, { recursive: true }); devDirCreated = true; } + + if (prod) writeFileSync(join(prodDir, `${id}.json`), JSON.stringify(prod, null, 2) + '\n'); + if (dev) writeFileSync(join(devDir, `${id}.json`), JSON.stringify(dev, null, 2) + '\n'); + written++; + } + totalWritten += written; + totalSkipped += skipped; + console.log(`${file}: ${written} differ, ${skipped} match (after removing diagnostics)`); +} +console.log(`\nDone. ${totalWritten} written, ${totalSkipped} skipped.`); diff --git a/utilities/split-by-system.js b/utilities/split-by-system.js new file mode 100644 index 0000000..814309d --- /dev/null +++ b/utilities/split-by-system.js @@ -0,0 +1,198 @@ +import { createReadStream, writeFileSync } from 'fs'; +import { createInterface } from 'readline'; +import { createHash } from 'crypto'; + +const inputFile = '/Users/grahamegrieve/temp/tx-comp/comparison.ndjson'; +const outDir = '/Users/grahamegrieve/temp/tx-comp/'; + +// Map well-known system URLs to short names +const systemNames = { + 'http://snomed.info/sct': 'snomed', + 'http://loinc.org': 'loinc', + 'http://unitsofmeasure.org': 'ucum', + 'http://hl7.org/fhir/sid/icd-10': 'icd10', + 'http://hl7.org/fhir/sid/icd-10-cm': 'icd10cm', + 'http://hl7.org/fhir/sid/icd-9-cm': 'icd9cm', + 'http://www.nlm.nih.gov/research/umls/rxnorm': 'rxnorm', + 'http://hl7.org/fhir/sid/ndc': 'ndc', + 'http://www.ama-assn.org/go/cpt': 'cpt', + 'urn:ietf:bcp:13': 'mimetypes', + 'urn:ietf:bcp:47': 'bcp47', + 'urn:iso:std:iso:3166': 'iso3166', + 'urn:iso:std:iso:4217': 'iso4217', +}; + +let unknownCounter = 0; +const unknownMap = new Map(); // url -> assigned name + +function nameForSystem(url) { + if (!url) return null; + // exact match + if (systemNames[url]) return systemNames[url]; + // check if it starts with a known prefix (for versioned URLs like snomed with editions) + for (const [key, name] of Object.entries(systemNames)) { + if (url.startsWith(key)) return name; + } + // try to derive a name from the URL + if (url.startsWith('http://hl7.org/fhir/')) { + // e.g. http://hl7.org/fhir/administrative-gender -> administrative-gender + const parts = url.replace('http://hl7.org/fhir/', '').split('/'); + const last = parts[parts.length - 1]; + if (last && last.length > 0 && last.length < 60) return 'fhir-' + last; + } + if (url.startsWith('http://terminology.hl7.org/')) { + const parts = url.replace('http://terminology.hl7.org/', '').split('/'); + const last = parts[parts.length - 1]; + if (last && last.length > 0 && last.length < 60) return 'tho-' + last; + } + if (url.startsWith('http://hl7.org/fhir/v2/')) { + return 'v2-' + url.replace('http://hl7.org/fhir/v2/', '').replace(/\//g, '-'); + } + if (url.startsWith('http://hl7.org/fhir/v3/') || url.startsWith('http://terminology.hl7.org/CodeSystem/v3-')) { + const tail = url.includes('v3/') ? url.split('v3/').pop() : url.split('v3-').pop(); + return 'v3-' + tail.replace(/\//g, '-'); + } + // fall back to numbered + if (unknownMap.has(url)) return unknownMap.get(url); + unknownCounter++; + const name = 'n' + String(unknownCounter).padStart(3, '0'); + unknownMap.set(url, name); + return name; +} + +function extractSystems(line) { + let obj; + try { obj = JSON.parse(line); } catch { return null; } + + const systems = new Set(); + const reqBody = obj.requestBody; + if (!reqBody) return { systems: new Set(), obj }; + + let req; + try { req = JSON.parse(reqBody); } catch { return { systems: new Set(), obj }; } + + // Walk the parameters looking for system values, codings, codeableConcepts + if (req.parameter) { + for (const p of req.parameter) { + if (p.name === 'system' && p.valueUri) { + systems.add(p.valueUri); + } + if (p.name === 'coding' && p.valueCoding?.system) { + systems.add(p.valueCoding.system); + } + if (p.name === 'codeableConcept' && p.valueCodeableConcept?.coding) { + for (const c of p.valueCodeableConcept.coding) { + if (c.system) systems.add(c.system); + } + } + if (p.name === 'url' && p.valueUri) { + // This is a ValueSet URL, not a system - skip + } + // For batch-validate, look inside nested resources + if (p.name === 'validation' && p.resource?.parameter) { + for (const inner of p.resource.parameter) { + if (inner.name === 'system' && inner.valueUri) systems.add(inner.valueUri); + if (inner.name === 'coding' && inner.valueCoding?.system) systems.add(inner.valueCoding.system); + if (inner.name === 'codeableConcept' && inner.valueCodeableConcept?.coding) { + for (const c of inner.valueCodeableConcept.coding) { + if (c.system) systems.add(c.system); + } + } + } + } + } + } + + // Also check if it's a Bundle (batch) + if (req.entry) { + for (const entry of req.entry) { + const res = entry.resource; + if (res?.parameter) { + for (const p of res.parameter) { + if (p.name === 'system' && p.valueUri) systems.add(p.valueUri); + if (p.name === 'coding' && p.valueCoding?.system) systems.add(p.valueCoding.system); + if (p.name === 'codeableConcept' && p.valueCodeableConcept?.coding) { + for (const c of p.valueCodeableConcept.coding) { + if (c.system) systems.add(c.system); + } + } + } + } + } + } + + return { systems, obj }; +} + +async function run() { + const seenHashes = new Set(); + const fileLines = new Map(); // filename -> lines[] + let totalLines = 0; + let dupes = 0; + let noSystem = 0; + + const rl = createInterface({ + input: createReadStream(inputFile), + crlfDelay: Infinity + }); + + for await (const line of rl) { + if (!line.trim()) continue; + totalLines++; + + // Hash the requestBody for dedup (not the whole line, since prod/dev responses differ) + let parsed; + try { parsed = JSON.parse(line); } catch { continue; } + + const hashInput = (parsed.method || '') + '|' + (parsed.url || '') + '|' + (parsed.requestBody || ''); + const hash = createHash('md5').update(hashInput).digest('hex'); + + if (seenHashes.has(hash)) { + dupes++; + continue; + } + seenHashes.add(hash); + + const result = extractSystems(line); + if (!result) continue; + + const { systems } = result; + + let filename; + if (systems.size === 0) { + noSystem++; + filename = 'system-unknown.ndjson'; + } else if (systems.size === 1) { + const sysUrl = [...systems][0]; + const name = nameForSystem(sysUrl); + filename = `system-${name}.ndjson`; + } else { + filename = 'system-multiple.ndjson'; + } + + if (!fileLines.has(filename)) fileLines.set(filename, []); + fileLines.get(filename).push(line); + + if (totalLines % 50000 === 0) { + console.log(` processed ${totalLines} lines...`); + } + } + + // Write all files + for (const [filename, lines] of fileLines) { + const path = outDir + filename; + writeFileSync(path, lines.join('\n') + '\n'); + console.log(` ${filename}: ${lines.length} entries`); + } + + console.log(`\nDone. ${totalLines} total lines, ${dupes} duplicates removed, ${noSystem} with no system found.`); + + // Write the unknown system mapping + if (unknownMap.size > 0) { + const mapping = Object.fromEntries(unknownMap); + writeFileSync(outDir + 'system-mapping.json', JSON.stringify(mapping, null, 2)); + console.log(`\nUnknown system mapping written to system-mapping.json (${unknownMap.size} systems)`); + } +} + +run().catch(e => { console.error(e); process.exit(1); });