From 225d702f702654d65d6994ef88e0dbce781aaa90 Mon Sep 17 00:00:00 2001 From: Mint Thompson Date: Mon, 8 Jan 2024 11:02:55 -0500 Subject: [PATCH] Support code caret rules in ValueSets (#249) * Support code caret rules in ValueSets A ValueSet can use caret rules with a code path to set values on included or excluded concepts. The typical use of this is to set a designation, but other elements may also appear. Elements outside of a concept are still unsupported and require the creation of an Instance. * update jest-extended dependency to avoid downstream problems * ValueSet concept caret rules include system in path array The SUSHI dependency is updated to 3.6.0, which supports including the system in the path array. This is needed for rules on ValueSets. Add the system to the path array when extracting caret rules on ValueSet concept components. Caret rules on CodeSystem concepts don't need a system, but still need the leading # as a separator. Add optimizer that reorders rules on ValueSets so that concept rules are grouped with their corresponding caret rules. Update other tests for Invariants, ValueSets, and ConceptRules based on other changes included in SUSHI 3.6.0. * Resolve URLs in ValueSet caret rule concept paths Resolve URLs on caret rules after resolving URLs on ValueSet components so that the caret rules will be able to use aliases that were created when resolving the components. Create new arrays to assign to path array when extracting caret rules on concepts. This avoids the possibility of inadvertantly modifying multiple rules with a single operation that mutates the path array. * Add concepts without caret rules to optimizer test --- package-lock.json | 506 ++++-------------- package.json | 4 +- src/extractor/CaretValueRuleExtractor.ts | 13 +- .../ResolveValueSetCaretRuleURLsOptimizer.ts | 29 + ...solveValueSetComponentRuleURLsOptimizer.ts | 2 +- ...SeparateConceptsWithCaretRulesOptimizer.ts | 71 +++ src/optimizer/plugins/index.ts | 4 + src/processor/CodeSystemProcessor.ts | 3 +- src/processor/ValueSetProcessor.ts | 28 +- .../ExportableCaretValueRule.test.ts | 25 + test/exportable/ExportableConceptRule.test.ts | 2 +- test/exportable/ExportableInvariant.test.ts | 12 +- .../ExportableValueSetComponentRule.test.ts | 39 +- .../extractor/CaretValueRuleExtractor.test.ts | 4 +- ...olveValueSetCaretRuleURLsOptimizer.test.ts | 108 ++++ ...ValueSetComponentRuleURLsOptimizer.test.ts | 4 +- ...ateConceptsWithCaretRulesOptimizer.test.ts | 102 ++++ .../fixtures/unsupported-valueset.json | 10 +- test/processor/CodeSystemProcessor.test.ts | 8 +- test/processor/ValueSetProcessor.test.ts | 72 ++- .../unsupported-valueset-missing-id.json | 10 +- .../fixtures/unsupported-valueset.json | 10 +- 22 files changed, 592 insertions(+), 474 deletions(-) create mode 100644 src/optimizer/plugins/ResolveValueSetCaretRuleURLsOptimizer.ts create mode 100644 src/optimizer/plugins/SeparateConceptsWithCaretRulesOptimizer.ts create mode 100644 test/optimizer/plugins/ResolveValueSetCaretRuleURLsOptimizer.test.ts create mode 100644 test/optimizer/plugins/SeparateConceptsWithCaretRulesOptimizer.test.ts diff --git a/package-lock.json b/package-lock.json index f5ec0899..79084159 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "fhir-package-loader": "^0.5.0", "flat": "^5.0.2", "fs-extra": "^9.0.1", - "fsh-sushi": "^3.5.0", + "fsh-sushi": "^3.6.0", "ini": "^1.3.8", "lodash": "^4.17.21", "readline-sync": "^1.4.10", @@ -56,7 +56,7 @@ "eslint": "^8.5.0", "eslint-config-prettier": "^6.10.1", "jest": "^28.1.3", - "jest-extended": "^1.2.0", + "jest-extended": "^3.0.2", "opener": "^1.5.1", "prettier": "^2.0.2", "ts-jest": "^28.0.7", @@ -1496,15 +1496,6 @@ "integrity": "sha512-+33x29mg+ecU88ODdWpqaie2upIuRkhujVLA7TuJjM823cNMbeggfI6NhxewaRaRF8dy+g33e4uIg/m5Mb3xDQ==", "dev": true }, - "node_modules/@types/yargs": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", - "integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/@types/yargs-parser": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", @@ -2353,12 +2344,12 @@ } }, "node_modules/diff-sequences": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.4.0.tgz", - "integrity": "sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/diff2html": { @@ -3223,9 +3214,9 @@ } }, "node_modules/fsh-sushi": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fsh-sushi/-/fsh-sushi-3.5.0.tgz", - "integrity": "sha512-Qd5ka92bwbWlH5fSYbFVZRTNn3QO+moXnARn2jCZ+S2veYDfkwkIys6BOUSQV6e0unm98zkk/DMmDHohhA0Z3A==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/fsh-sushi/-/fsh-sushi-3.6.0.tgz", + "integrity": "sha512-G6JLb3FTnVrUkFppXzhUGDJcD0s3pljlS5iq+0P1UIvOxI+pR9qKVTZXELh2vDf4tDwNUkIiK33AD+njHKihJg==", "dependencies": { "ajv": "^8.12.0", "antlr4": "~4.13.1", @@ -3233,7 +3224,7 @@ "chalk": "^3.0.0", "commander": "^8.2.0", "fhir": "^4.9.0", - "fhir-package-loader": "^0.6.0", + "fhir-package-loader": "^0.7.0", "fs-extra": "^8.1.0", "html-minifier-terser": "5.1.1", "https-proxy-agent": "^5.0.0", @@ -3244,6 +3235,7 @@ "sanitize-filename": "^1.6.3", "sax": "^1.2.4", "temp": "^0.9.1", + "text-table": "^0.2.0", "title-case": "^3.0.2", "valid-url": "^1.0.9", "winston": "^3.3.3", @@ -3366,9 +3358,9 @@ } }, "node_modules/fsh-sushi/node_modules/fhir-package-loader": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-0.6.0.tgz", - "integrity": "sha512-rqO2Iiz7rCopNbPbEF59j4jOMScb5fCnf/gdgLFpsWvkGsnalbV/Q+T3E4bRPjdvwhuyZAoUJezwnvEfrQfnZg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-0.7.0.tgz", + "integrity": "sha512-Zd9A1yohoCNTMEp/vO4YiBmqvkdQgR7fxx0/7jqa6/Rd8JkYPjyXby137SHNdluRs+Wv1tUdztKfzYNWZv9bYg==", "dependencies": { "axios": "^0.21.1", "chalk": "^4.1.2", @@ -3425,9 +3417,9 @@ } }, "node_modules/fsh-sushi/node_modules/fhir-package-loader/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "engines": { "node": ">= 10.0.0" } @@ -4433,18 +4425,18 @@ } }, "node_modules/jest-diff": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.4.2.tgz", - "integrity": "sha512-ujc9ToyUZDh9KcqvQDkk/gkbf6zSaeEg9AiBxtttXW59H/AcqEYp1ciXAtJp+jXWva5nAf/ePtSsgWwE5mqp4Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^27.4.0", - "jest-get-type": "^27.4.0", - "pretty-format": "^27.4.2" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-docblock": { @@ -4529,158 +4521,33 @@ } }, "node_modules/jest-extended": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-1.2.0.tgz", - "integrity": "sha512-KYc5DgD+/8viJSEKBzb1vRXe/rEEQUxEovBTdNEer9A6lzvHvhuyslM5tQFBz8TbLEkicCmsEcQF+4N7GiPTLg==", - "dev": true, - "dependencies": { - "expect": "^26.6.2", - "jest-diff": "^27.2.5", - "jest-get-type": "^27.0.6", - "jest-matcher-utils": "^27.2.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/jest-extended/node_modules/@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-extended/node_modules/diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-extended/node_modules/expect": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", - "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", - "dev": true, - "dependencies": { - "@jest/types": "^26.6.2", - "ansi-styles": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-regex-util": "^26.0.0" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-extended/node_modules/expect/node_modules/jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-extended/node_modules/expect/node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-extended/node_modules/expect/node_modules/jest-matcher-utils": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", - "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-3.2.4.tgz", + "integrity": "sha512-lSEYhSmvXZG/7YXI7KO3LpiUiQ90gi5giwCJNDMMsX5a+/NZhdbQF2G4ALOBN+KcXVT3H6FPVPohAuMXooaLTQ==", "dev": true, "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "jest-diff": "^29.0.0", + "jest-get-type": "^29.0.0" }, "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-extended/node_modules/jest-message-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^26.6.2", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "pretty-format": "^26.6.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.2" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-extended/node_modules/jest-regex-util": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", - "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-extended/node_modules/pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dev": true, - "dependencies": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^17.0.1" + "peerDependencies": { + "jest": ">=27.2.5" }, - "engines": { - "node": ">= 10" + "peerDependenciesMeta": { + "jest": { + "optional": true + } } }, - "node_modules/jest-extended/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, "node_modules/jest-get-type": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.4.0.tgz", - "integrity": "sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { @@ -4757,21 +4624,6 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/jest-matcher-utils": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.4.2.tgz", - "integrity": "sha512-jyP28er3RRtMv+fmYC/PKG8wvAmfGcSNproVTW2Y0P/OY7/hWUOmsPfxN1jOhM+0u2xU984u2yEagGivz9OBGQ==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^27.4.2", - "jest-get-type": "^27.4.0", - "pretty-format": "^27.4.2" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/jest-message-util": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", @@ -5825,44 +5677,36 @@ } }, "node_modules/pretty-format": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.4.2.tgz", - "integrity": "sha512-p0wNtJ9oLuvgOQDEIZ9zQjZffK7KtyR6Si0jnXULIDwrlNF8Cuir3AZP0hHv0jmKuNN/edOnbMjnzd4uTcmWiw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/types": "^27.4.2", - "ansi-regex": "^5.0.1", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" + "react-is": "^18.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/pretty-format/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "node_modules/pretty-format/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" + "@sinclair/typebox": "^0.27.8" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/pretty-format/node_modules/@types/yargs": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", - "integrity": "sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } + "node_modules/pretty-format/node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", @@ -5876,12 +5720,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -8178,15 +8016,6 @@ "integrity": "sha512-+33x29mg+ecU88ODdWpqaie2upIuRkhujVLA7TuJjM823cNMbeggfI6NhxewaRaRF8dy+g33e4uIg/m5Mb3xDQ==", "dev": true }, - "@types/yargs": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", - "integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "@types/yargs-parser": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", @@ -8780,9 +8609,9 @@ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" }, "diff-sequences": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.4.0.tgz", - "integrity": "sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true }, "diff2html": { @@ -9428,9 +9257,9 @@ "optional": true }, "fsh-sushi": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fsh-sushi/-/fsh-sushi-3.5.0.tgz", - "integrity": "sha512-Qd5ka92bwbWlH5fSYbFVZRTNn3QO+moXnARn2jCZ+S2veYDfkwkIys6BOUSQV6e0unm98zkk/DMmDHohhA0Z3A==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/fsh-sushi/-/fsh-sushi-3.6.0.tgz", + "integrity": "sha512-G6JLb3FTnVrUkFppXzhUGDJcD0s3pljlS5iq+0P1UIvOxI+pR9qKVTZXELh2vDf4tDwNUkIiK33AD+njHKihJg==", "requires": { "ajv": "^8.12.0", "antlr4": "~4.13.1", @@ -9438,7 +9267,7 @@ "chalk": "^3.0.0", "commander": "^8.2.0", "fhir": "^4.9.0", - "fhir-package-loader": "^0.6.0", + "fhir-package-loader": "^0.7.0", "fs-extra": "^8.1.0", "html-minifier-terser": "5.1.1", "https-proxy-agent": "^5.0.0", @@ -9449,6 +9278,7 @@ "sanitize-filename": "^1.6.3", "sax": "^1.2.4", "temp": "^0.9.1", + "text-table": "^0.2.0", "title-case": "^3.0.2", "valid-url": "^1.0.9", "winston": "^3.3.3", @@ -9586,9 +9416,9 @@ } }, "fhir-package-loader": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-0.6.0.tgz", - "integrity": "sha512-rqO2Iiz7rCopNbPbEF59j4jOMScb5fCnf/gdgLFpsWvkGsnalbV/Q+T3E4bRPjdvwhuyZAoUJezwnvEfrQfnZg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/fhir-package-loader/-/fhir-package-loader-0.7.0.tgz", + "integrity": "sha512-Zd9A1yohoCNTMEp/vO4YiBmqvkdQgR7fxx0/7jqa6/Rd8JkYPjyXby137SHNdluRs+Wv1tUdztKfzYNWZv9bYg==", "requires": { "axios": "^0.21.1", "chalk": "^4.1.2", @@ -9631,9 +9461,9 @@ } }, "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" } } }, @@ -10344,15 +10174,15 @@ } }, "jest-diff": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.4.2.tgz", - "integrity": "sha512-ujc9ToyUZDh9KcqvQDkk/gkbf6zSaeEg9AiBxtttXW59H/AcqEYp1ciXAtJp+jXWva5nAf/ePtSsgWwE5mqp4Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "requires": { "chalk": "^4.0.0", - "diff-sequences": "^27.4.0", - "jest-get-type": "^27.4.0", - "pretty-format": "^27.4.2" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" } }, "jest-docblock": { @@ -10418,129 +10248,19 @@ } }, "jest-extended": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-1.2.0.tgz", - "integrity": "sha512-KYc5DgD+/8viJSEKBzb1vRXe/rEEQUxEovBTdNEer9A6lzvHvhuyslM5tQFBz8TbLEkicCmsEcQF+4N7GiPTLg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-3.2.4.tgz", + "integrity": "sha512-lSEYhSmvXZG/7YXI7KO3LpiUiQ90gi5giwCJNDMMsX5a+/NZhdbQF2G4ALOBN+KcXVT3H6FPVPohAuMXooaLTQ==", "dev": true, "requires": { - "expect": "^26.6.2", - "jest-diff": "^27.2.5", - "jest-get-type": "^27.0.6", - "jest-matcher-utils": "^27.2.4" - }, - "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true - }, - "expect": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", - "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "ansi-styles": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-regex-util": "^26.0.0" - }, - "dependencies": { - "jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - } - }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, - "jest-matcher-utils": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", - "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - } - } - } - }, - "jest-message-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^26.6.2", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "pretty-format": "^26.6.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.2" - } - }, - "jest-regex-util": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", - "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", - "dev": true - }, - "pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^17.0.1" - } - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - } + "jest-diff": "^29.0.0", + "jest-get-type": "^29.0.0" } }, "jest-get-type": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.4.0.tgz", - "integrity": "sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true }, "jest-haste-map": { @@ -10599,18 +10319,6 @@ } } }, - "jest-matcher-utils": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.4.2.tgz", - "integrity": "sha512-jyP28er3RRtMv+fmYC/PKG8wvAmfGcSNproVTW2Y0P/OY7/hWUOmsPfxN1jOhM+0u2xU984u2yEagGivz9OBGQ==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^27.4.2", - "jest-get-type": "^27.4.0", - "pretty-format": "^27.4.2" - } - }, "jest-message-util": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", @@ -11427,50 +11135,36 @@ "dev": true }, "pretty-format": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.4.2.tgz", - "integrity": "sha512-p0wNtJ9oLuvgOQDEIZ9zQjZffK7KtyR6Si0jnXULIDwrlNF8Cuir3AZP0hHv0jmKuNN/edOnbMjnzd4uTcmWiw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "requires": { - "@jest/types": "^27.4.2", - "ansi-regex": "^5.0.1", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" + "react-is": "^18.0.0" }, "dependencies": { - "@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" + "@sinclair/typebox": "^0.27.8" } }, - "@types/yargs": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", - "integrity": "sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true }, "ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true } } }, diff --git a/package.json b/package.json index 81ae2663..44c80d21 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "eslint": "^8.5.0", "eslint-config-prettier": "^6.10.1", "jest": "^28.1.3", - "jest-extended": "^1.2.0", + "jest-extended": "^3.0.2", "opener": "^1.5.1", "prettier": "^2.0.2", "ts-jest": "^28.0.7", @@ -76,7 +76,7 @@ "fhir-package-loader": "^0.5.0", "flat": "^5.0.2", "fs-extra": "^9.0.1", - "fsh-sushi": "^3.5.0", + "fsh-sushi": "^3.6.0", "ini": "^1.3.8", "lodash": "^4.17.21", "readline-sync": "^1.4.10", diff --git a/src/extractor/CaretValueRuleExtractor.ts b/src/extractor/CaretValueRuleExtractor.ts index 47d78fa9..204f80be 100644 --- a/src/extractor/CaretValueRuleExtractor.ts +++ b/src/extractor/CaretValueRuleExtractor.ts @@ -242,8 +242,9 @@ export class CaretValueRuleExtractor { static processConcept( input: fhirtypes.CodeSystemConcept, - conceptHierarchy: string[], - codeSystemName: string, + pathArray: string[], + entityName: string, + entityType: 'CodeSystem' | 'ValueSet', fisher: utils.Fishable ): ExportableCaretValueRule[] { const caretValueRules: ExportableCaretValueRule[] = []; @@ -258,12 +259,12 @@ export class CaretValueRuleExtractor { caretValueRule.caretPath = key; caretValueRule.value = getFSHValue(i, flatArray, 'Concept', fisher); caretValueRule.isCodeCaretRule = true; - caretValueRule.pathArray = conceptHierarchy; + caretValueRule.pathArray = [...pathArray]; if (isFSHValueEmpty(caretValueRule.value)) { logger.error( - `Value in CodeSytem ${codeSystemName} at concept ${conceptHierarchy.join( - '.' - )} for element ${caretValueRule.caretPath} is empty. No caret value rule will be created.` + `Value in ${entityType} ${entityName} at concept ${pathArray.join('.')} for element ${ + caretValueRule.caretPath + } is empty. No caret value rule will be created.` ); } else { caretValueRules.push(caretValueRule); diff --git a/src/optimizer/plugins/ResolveValueSetCaretRuleURLsOptimizer.ts b/src/optimizer/plugins/ResolveValueSetCaretRuleURLsOptimizer.ts new file mode 100644 index 00000000..04b3782e --- /dev/null +++ b/src/optimizer/plugins/ResolveValueSetCaretRuleURLsOptimizer.ts @@ -0,0 +1,29 @@ +import { utils } from 'fsh-sushi'; +import { OptimizerPlugin } from '../OptimizerPlugin'; +import { optimizeURL } from '../utils'; +import { Package } from '../../processor'; +import { MasterFisher, ProcessingOptions } from '../../utils'; +import { ExportableCaretValueRule } from '../../exportable'; + +export default { + name: 'resolve_value_set_caret_rule_urls', + description: 'Replace URLs in value set caret rules with their names or aliases', + runAfter: ['resolve_value_set_component_rule_urls'], + optimize(pkg: Package, fisher: MasterFisher, options: ProcessingOptions = {}): void { + pkg.valueSets.forEach(vs => { + vs.rules.forEach(rule => { + if (rule instanceof ExportableCaretValueRule && rule.pathArray.length > 0) { + const [system, ...code] = rule.pathArray[0].split('#'); + const resolvedSystem = optimizeURL( + system, + pkg.aliases, + [utils.Type.CodeSystem], + fisher, + options.alias ?? true + ); + rule.pathArray[0] = [resolvedSystem, code.join('#')].join('#'); + } + }); + }); + } +} as OptimizerPlugin; diff --git a/src/optimizer/plugins/ResolveValueSetComponentRuleURLsOptimizer.ts b/src/optimizer/plugins/ResolveValueSetComponentRuleURLsOptimizer.ts index fa073467..4ddf6d58 100644 --- a/src/optimizer/plugins/ResolveValueSetComponentRuleURLsOptimizer.ts +++ b/src/optimizer/plugins/ResolveValueSetComponentRuleURLsOptimizer.ts @@ -10,7 +10,7 @@ import { export default { name: 'resolve_value_set_component_rule_urls', - description: 'Replace URLs in value set rules with their names or aliases', + description: 'Replace URLs in value set component rules with their names or aliases', optimize(pkg: Package, fisher: MasterFisher, options: ProcessingOptions = {}): void { pkg.valueSets.forEach(vs => { diff --git a/src/optimizer/plugins/SeparateConceptsWithCaretRulesOptimizer.ts b/src/optimizer/plugins/SeparateConceptsWithCaretRulesOptimizer.ts new file mode 100644 index 00000000..23db30cb --- /dev/null +++ b/src/optimizer/plugins/SeparateConceptsWithCaretRulesOptimizer.ts @@ -0,0 +1,71 @@ +import { isEmpty, isEqual } from 'lodash'; +import { ExportableCaretValueRule, ExportableValueSetConceptComponentRule } from '../../exportable'; +import { Package } from '../../processor'; +import { OptimizerPlugin } from '../OptimizerPlugin'; + +// a ValueSetConceptComponentRule will print as multiple consecutive rules +// if there is a system, but no valuesets. +// normally, this is fine, but if more than one of those concepts has caret rules, +// split them manually so that the caret rules appear immediately after the concept. for example: +// * #BEAR from system http://example.org/zoo +// * #BEAR ^designation.value = "ourse" +// * #BEAR ^designation.language = #fr +// * #PEL from system http://example.org/zoo +// * #PEL ^designation.value = "pelícano" +// * #PEL ^designation.language = #es +export default { + name: 'separate_concepts_with_caret_rules', + description: 'Separate concepts in ValueSets from the same system if they also have caret rules.', + runBefore: ['resolve_value_set_component_rule_urls'], + optimize(pkg: Package): void { + pkg.valueSets.forEach(vs => { + const systemRulesToCheck = vs.rules.filter(rule => { + return ( + rule instanceof ExportableValueSetConceptComponentRule && + rule.from.system != null && + isEmpty(rule.from.valueSets) && + rule.concepts.length > 1 + ); + }) as ExportableValueSetConceptComponentRule[]; + const allCodeCaretRules = vs.rules.filter(rule => { + return rule instanceof ExportableCaretValueRule && rule.pathArray.length > 0; + }) as ExportableCaretValueRule[]; + if (allCodeCaretRules.length > 0) { + systemRulesToCheck.forEach(conceptRule => { + // for each concept in the rule, see if there are any caret value rules. + const caretRulesForSystem = new Map(); + conceptRule.concepts.forEach(concept => { + caretRulesForSystem.set( + concept.code, + allCodeCaretRules.filter(caretRule => + isEqual(caretRule.pathArray, [`${conceptRule.from.system ?? ''}#${concept.code}`]) + ) + ); + }); + if (caretRulesForSystem.size > 1) { + // split apart the codes so that the ones with caret rules can be next to their concept rule + const reorganizedRules: ( + | ExportableValueSetConceptComponentRule + | ExportableCaretValueRule + )[] = []; + for (const concept of conceptRule.concepts) { + const singleConceptRule = new ExportableValueSetConceptComponentRule( + conceptRule.inclusion + ); + singleConceptRule.from.system = conceptRule.from.system; + singleConceptRule.concepts = [concept]; + // don't need to copy indent since it will always be 0 + reorganizedRules.push(singleConceptRule); + for (const caretRule of caretRulesForSystem.get(concept.code)) { + reorganizedRules.push(caretRule); + vs.rules.splice(vs.rules.indexOf(caretRule), 1); + } + } + const originalConceptRuleIndex = vs.rules.indexOf(conceptRule); + vs.rules.splice(originalConceptRuleIndex, 1, ...reorganizedRules); + } + }); + } + }); + } +} as OptimizerPlugin; diff --git a/src/optimizer/plugins/index.ts b/src/optimizer/plugins/index.ts index 43218cba..5abf0712 100644 --- a/src/optimizer/plugins/index.ts +++ b/src/optimizer/plugins/index.ts @@ -21,7 +21,9 @@ import ResolveOnlyRuleURLsOptimizer from './ResolveOnlyRuleURLsOptimizer'; import ResolveParentURLsOptimizer from './ResolveParentURLsOptimizer'; import ResolveReferenceAssignmentsOptimizer from './ResolveReferenceAssignmentsOptimizer'; import ResolveValueRuleURLsOptimizer from './ResolveValueRuleURLsOptimizer'; +import ResolveValueSetCaretRuleURLsOptimizer from './ResolveValueSetCaretRuleURLsOptimizer'; import ResolveValueSetComponentRuleURLsOptimizer from './ResolveValueSetComponentRuleURLsOptimizer'; +import SeparateConceptsWithCaretRulesOptimizer from './SeparateConceptsWithCaretRulesOptimizer'; import SimplifyArrayIndexingOptimizer from './SimplifyArrayIndexingOptimizer'; import SimplifyInstanceNameOptimizer from './SimplifyInstanceNameOptimizer'; import SimplifyMappingNamesOptimizer from './SimplifyMappingNamesOptimizer'; @@ -52,7 +54,9 @@ export { ResolveParentURLsOptimizer, ResolveReferenceAssignmentsOptimizer, ResolveValueRuleURLsOptimizer, + ResolveValueSetCaretRuleURLsOptimizer, ResolveValueSetComponentRuleURLsOptimizer, + SeparateConceptsWithCaretRulesOptimizer, SimplifyArrayIndexingOptimizer, SimplifyInstanceNameOptimizer, SimplifyMappingNamesOptimizer, diff --git a/src/processor/CodeSystemProcessor.ts b/src/processor/CodeSystemProcessor.ts index 2e1f9cbd..04a8b33d 100644 --- a/src/processor/CodeSystemProcessor.ts +++ b/src/processor/CodeSystemProcessor.ts @@ -54,8 +54,9 @@ export class CodeSystemProcessor { newConceptRule, ...CaretValueRuleExtractor.processConcept( concept, - [...newConceptRule.hierarchy, concept.code], + [...newConceptRule.hierarchy, concept.code].map(code => `#${code}`), codeSystemName, + 'CodeSystem', fisher ) ); diff --git a/src/processor/ValueSetProcessor.ts b/src/processor/ValueSetProcessor.ts index 8c3dd8db..3e15781b 100644 --- a/src/processor/ValueSetProcessor.ts +++ b/src/processor/ValueSetProcessor.ts @@ -13,8 +13,6 @@ const SUPPORTED_COMPONENT_PATHS = [ 'system', 'version', 'concept', - 'concept.code', - 'concept.display', 'filter', 'filter.property', 'filter.op', @@ -46,13 +44,33 @@ export class ValueSetProcessor { ...CaretValueRuleExtractor.processResource(input, fisher, input.resourceType, config) ); if (input.compose) { - input.compose.include?.forEach((vsComponent: any) => { + input.compose.include?.forEach((vsComponent: fhirtypes.ValueSetComposeIncludeOrExclude) => { newRules.push(ValueSetFilterComponentRuleExtractor.process(vsComponent, input, true)); newRules.push(ValueSetConceptComponentRuleExtractor.process(vsComponent, true)); + vsComponent.concept?.forEach(includedConcept => { + const conceptCaretRules = CaretValueRuleExtractor.processConcept( + includedConcept, + [`${vsComponent.system ?? ''}#${includedConcept.code}`], + target.name, + 'ValueSet', + fisher + ); + newRules.push(...conceptCaretRules); + }); }); - input.compose.exclude?.forEach((vsComponent: any) => { + input.compose.exclude?.forEach((vsComponent: fhirtypes.ValueSetComposeIncludeOrExclude) => { newRules.push(ValueSetFilterComponentRuleExtractor.process(vsComponent, input, false)); newRules.push(ValueSetConceptComponentRuleExtractor.process(vsComponent, false)); + vsComponent.concept?.forEach(excludedConcept => { + const conceptCaretRules = CaretValueRuleExtractor.processConcept( + excludedConcept, + [`${vsComponent.system ?? ''}#${excludedConcept.code}`], + target.name, + 'ValueSet', + fisher + ); + newRules.push(...conceptCaretRules); + }); }); } target.rules = compact(newRules); @@ -100,6 +118,8 @@ export class ValueSetProcessor { .filter(k => isNaN(parseInt(k))) .join('.'); }); + // any path that starts with "concept." is okay, since those can use code caret rules + flatPaths = flatPaths.filter(p => !p.startsWith('concept.')); // Check if there are any paths that are not a supported path return difference(flatPaths, SUPPORTED_COMPONENT_PATHS).length === 0; } diff --git a/test/exportable/ExportableCaretValueRule.test.ts b/test/exportable/ExportableCaretValueRule.test.ts index 4ce1884b..3b835e3e 100644 --- a/test/exportable/ExportableCaretValueRule.test.ts +++ b/test/exportable/ExportableCaretValueRule.test.ts @@ -51,6 +51,31 @@ describe('ExportableCaretValueRule', () => { expect(rule.toFSH()).toBe('* . ^short = "Another important summary."'); }); + it('should export a code caret rule with a code path', () => { + // this type of rule appears on CodeSystems + const rule = new ExportableCaretValueRule(''); + rule.isCodeCaretRule = true; + rule.caretPath = 'designation.value'; + rule.pathArray = ['#bear', '#brown bear']; + rule.value = 'Brown Bear'; + + expect(rule.toFSH()).toBe('* #bear #"brown bear" ^designation.value = "Brown Bear"'); + }); + + it('should export a code caret rule with a code and system path', () => { + // this type of rule appears on ValueSets + const rule = new ExportableCaretValueRule(''); + rule.isCodeCaretRule = true; + rule.caretPath = 'designation.value'; + rule.pathArray = ['http://example.org/zoo#brown bear']; + // rule.fromSystem = 'http://example.org/zoo'; + rule.value = 'Brown Bear'; + + expect(rule.toFSH()).toBe( + '* http://example.org/zoo#"brown bear" ^designation.value = "Brown Bear"' + ); + }); + it('should export a caret rule assigning a boolean', () => { const rule = new ExportableCaretValueRule(''); rule.caretPath = 'abstract'; diff --git a/test/exportable/ExportableConceptRule.test.ts b/test/exportable/ExportableConceptRule.test.ts index 64b22c62..264f85fd 100644 --- a/test/exportable/ExportableConceptRule.test.ts +++ b/test/exportable/ExportableConceptRule.test.ts @@ -63,7 +63,7 @@ describe('ExportableConceptRule', () => { rule.display = 'bar'; rule.definition = 'baz'; - const expectedResult = '* #"foo\twith\ta\ttab" "bar" "baz"'; + const expectedResult = '* #"foo\\twith\\ta\\ttab" "bar" "baz"'; const result = rule.toFSH(); expect(result).toBe(expectedResult); }); diff --git a/test/exportable/ExportableInvariant.test.ts b/test/exportable/ExportableInvariant.test.ts index 58f519eb..73984fbd 100644 --- a/test/exportable/ExportableInvariant.test.ts +++ b/test/exportable/ExportableInvariant.test.ts @@ -21,9 +21,9 @@ describe('ExportableInvariant', () => { const expectedResult = [ 'Invariant: inv-2', 'Description: "This is an important condition."', - 'Severity: #error', - 'Expression: "requirement.exists()"', - 'XPath: "f:requirement"' + '* severity = #error', + '* expression = "requirement.exists()"', + '* xpath = "f:requirement"' ].join(EOL); const result = input.toFSH(); expect(result).toBe(expectedResult); @@ -39,9 +39,9 @@ describe('ExportableInvariant', () => { const expectedResult = [ 'Invariant: inv-3', 'Description: """Please do this.\nPlease always do this with a \\ character."""', - 'Severity: #warning', - 'Expression: "requirement.contains(\\"\\\\\\")"', - 'XPath: "f:requirement"' + '* severity = #warning', + '* expression = "requirement.contains(\\"\\\\\\")"', + '* xpath = "f:requirement"' ].join(EOL); const result = input.toFSH(); expect(result).toBe(expectedResult); diff --git a/test/exportable/ExportableValueSetComponentRule.test.ts b/test/exportable/ExportableValueSetComponentRule.test.ts index 761ced9d..f3c571e0 100644 --- a/test/exportable/ExportableValueSetComponentRule.test.ts +++ b/test/exportable/ExportableValueSetComponentRule.test.ts @@ -60,7 +60,7 @@ describe('ExportableValueSetConceptComponentRule', () => { rule.concepts.push(new fshtypes.FshCode('foo', 'bar')); rule.from.valueSets = ['someValueSet']; - expect(rule.toFSH()).toBe('* include bar#foo from valueset someValueSet'); + expect(rule.toFSH()).toBe('* bar#foo from valueset someValueSet'); }); it('should export a ValueSetConceptComponentRule with a concept excluded from a valueset', () => { @@ -76,7 +76,7 @@ describe('ExportableValueSetConceptComponentRule', () => { rule.concepts.push(new fshtypes.FshCode('foo', 'bar')); rule.from.valueSets = ['someValueSet', 'otherValueSet']; - expect(rule.toFSH()).toBe('* include bar#foo from valueset someValueSet and otherValueSet'); + expect(rule.toFSH()).toBe('* bar#foo from valueset someValueSet and otherValueSet'); }); it('should export a ValueSetConceptComponentRule with a concept excluded from several valuesets', () => { @@ -89,23 +89,21 @@ describe('ExportableValueSetConceptComponentRule', () => { it('should export a ValueSetConceptComponentRule with a concept included from a system and several valuesets', () => { const rule = new ExportableValueSetConceptComponentRule(true); - rule.concepts.push(new fshtypes.FshCode('foo')); + rule.concepts.push(new fshtypes.FshCode('foo', 'someSystem')); rule.from.system = 'someSystem'; rule.from.valueSets = ['someValueSet', 'otherValueSet']; - expect(rule.toFSH()).toBe( - '* include #foo from system someSystem and valueset someValueSet and otherValueSet' - ); + expect(rule.toFSH()).toBe('* someSystem#foo from valueset someValueSet and otherValueSet'); }); it('should export a ValueSetConceptComponentRule with a concept excluded from a system and several valuesets', () => { const rule = new ExportableValueSetConceptComponentRule(false); - rule.concepts.push(new fshtypes.FshCode('foo')); + rule.concepts.push(new fshtypes.FshCode('foo', 'someSystem')); rule.from.system = 'someSystem'; rule.from.valueSets = ['someValueSet', 'otherValueSet']; expect(rule.toFSH()).toBe( - '* exclude #foo from system someSystem and valueset someValueSet and otherValueSet' + '* exclude someSystem#foo from valueset someValueSet and otherValueSet' ); }); }); @@ -273,31 +271,6 @@ describe('ExportableValueSetFilterComponentRule', () => { ); }); - it('should format a long ValueSetConceptComponentRule to take up multiple lines', () => { - const rule = new ExportableValueSetConceptComponentRule(true); - rule.concepts = [ - new FshCode('cookies', undefined, 'Cookies'), - new FshCode('candy', undefined, 'Candy'), - new FshCode('chips', undefined, 'Chips'), - new FshCode('cakes', undefined, 'Cakes'), - new FshCode('verylargecakes', undefined, 'Very Large Cakes') - ]; - rule.from.system = 'http://fhir.food-pyramid.org/FoodPyramidGuide/CodeSystems/FoodGroupsCS'; - rule.from.valueSets = ['http://fhir.food-pyramid.org/FoodPyramidGuide/ValueSets/DeliciousVS']; - - const result = rule.toFSH(); - const expectedResult = [ - '* include #cookies "Cookies" and', - ' #candy "Candy" and', - ' #chips "Chips" and', - ' #cakes "Cakes" and', - ' #verylargecakes "Very Large Cakes"', - ' from system http://fhir.food-pyramid.org/FoodPyramidGuide/CodeSystems/FoodGroupsCS and', - ' valueset http://fhir.food-pyramid.org/FoodPyramidGuide/ValueSets/DeliciousVS' - ].join(EOL); - expect(result).toEqual(expectedResult); - }); - it('should format a long ValueSetFilterComponentRule to take up multiple lines', () => { const rule = new ExportableValueSetFilterComponentRule(false); rule.from.system = 'http://fhir.example.org/myImplementationGuide/CodeSystem/AppleCS'; diff --git a/test/extractor/CaretValueRuleExtractor.test.ts b/test/extractor/CaretValueRuleExtractor.test.ts index 9eaffe0b..25fa5986 100644 --- a/test/extractor/CaretValueRuleExtractor.test.ts +++ b/test/extractor/CaretValueRuleExtractor.test.ts @@ -643,6 +643,7 @@ describe('CaretValueRuleExtractor', () => { testConcept, ['testConcept'], 'testCS', + 'CodeSystem', defs ); expect(caretRules).toContainEqual( @@ -683,6 +684,7 @@ describe('CaretValueRuleExtractor', () => { testConcept, ['testConcept'], 'testCS', + 'CodeSystem', defs ); @@ -693,7 +695,7 @@ describe('CaretValueRuleExtractor', () => { }) ); expect(loggerSpy.getLastMessage('error')).toMatch( - 'Value in CodeSytem testCS at concept testConcept for element property[0] is empty. No caret value rule will be created.' + 'Value in CodeSystem testCS at concept testConcept for element property[0] is empty. No caret value rule will be created.' ); }); }); diff --git a/test/optimizer/plugins/ResolveValueSetCaretRuleURLsOptimizer.test.ts b/test/optimizer/plugins/ResolveValueSetCaretRuleURLsOptimizer.test.ts new file mode 100644 index 00000000..8173358a --- /dev/null +++ b/test/optimizer/plugins/ResolveValueSetCaretRuleURLsOptimizer.test.ts @@ -0,0 +1,108 @@ +import path from 'path'; +import '../../helpers/loggerSpy'; // side-effect: suppresses logs +import { LakeOfFHIR, Package } from '../../../src/processor'; +import { ExportableValueSet, ExportableCaretValueRule } from '../../../src/exportable'; +import { FHIRDefinitions, MasterFisher } from '../../../src/utils'; +import { loadTestDefinitions, stockLake } from '../../helpers'; +import optimizer from '../../../src/optimizer/plugins/ResolveValueSetCaretRuleURLsOptimizer'; + +describe('optimizer', () => { + describe('#resolve_value_set_caret_rule_urls', () => { + let defs: FHIRDefinitions; + let lake: LakeOfFHIR; + let fisher: MasterFisher; + + beforeAll(() => { + defs = loadTestDefinitions(); + lake = stockLake(path.join(__dirname, 'fixtures', 'simple-codesystem.json')); + fisher = new MasterFisher(lake, defs); + }); + + it('should have appropriate metadata', () => { + expect(optimizer.name).toBe('resolve_value_set_caret_rule_urls'); + expect(optimizer.description).toBeDefined(); + expect(optimizer.runBefore).toBeUndefined(); + expect(optimizer.runAfter).toEqual(['resolve_value_set_component_rule_urls']); + }); + + it('should replace caret rule system url with the name of a local CodeSystem', () => { + const valueset = new ExportableValueSet('MyValueSet'); + const rule = new ExportableCaretValueRule(''); + rule.caretPath = 'designation.use.display'; + rule.pathArray = ['http://example.org/tests/CodeSystem/simple.codesystem#toast']; + rule.value = 'Complete bread classification'; + valueset.rules.push(rule); + const myPackage = new Package(); + myPackage.add(valueset); + optimizer.optimize(myPackage, fisher); + + expect(rule.pathArray).toEqual(['SimpleCodeSystem#toast']); + }); + + it('should replace caret rule system url with the name of a core FHIR CodeSystem', () => { + const valueset = new ExportableValueSet('MyValueSet'); + const rule = new ExportableCaretValueRule(''); + rule.caretPath = 'designation.use.display'; + rule.pathArray = ['http://hl7.org/fhir/observation-status#draft']; + rule.value = 'Complete bread classification'; + valueset.rules.push(rule); + const myPackage = new Package(); + myPackage.add(valueset); + optimizer.optimize(myPackage, fisher); + + expect(rule.pathArray).toEqual(['ObservationStatus#draft']); + }); + + it('should replace caret rule system url with an alias when the system definition is not known and alias is true', () => { + const valueset = new ExportableValueSet('MyValueSet'); + const rule = new ExportableCaretValueRule(''); + rule.caretPath = 'designation.use.display'; + rule.pathArray = ['http://example.org/tests/CodeSystem/mystery.codesystem#toast']; + rule.value = 'Complete bread classification'; + valueset.rules.push(rule); + const myPackage = new Package(); + myPackage.add(valueset); + optimizer.optimize(myPackage, fisher, { alias: true }); + + const mysteryAlias = myPackage.aliases.find( + alias => alias.url === 'http://example.org/tests/CodeSystem/mystery.codesystem' + ); + expect(mysteryAlias).toBeDefined(); + expect(rule.pathArray).toEqual([`${mysteryAlias?.alias}#toast`]); + }); + + it('should replace caret rule system url with an alias when the system definition is not known and alias is undefined', () => { + const valueset = new ExportableValueSet('MyValueSet'); + const rule = new ExportableCaretValueRule(''); + rule.caretPath = 'designation.use.display'; + rule.pathArray = ['http://example.org/tests/CodeSystem/mystery.codesystem#toast']; + rule.value = 'Complete bread classification'; + valueset.rules.push(rule); + const myPackage = new Package(); + myPackage.add(valueset); + optimizer.optimize(myPackage, fisher); + + const mysteryAlias = myPackage.aliases.find( + alias => alias.url === 'http://example.org/tests/CodeSystem/mystery.codesystem' + ); + expect(mysteryAlias).toBeDefined(); + expect(rule.pathArray).toEqual([`${mysteryAlias?.alias}#toast`]); + }); + + it('should not replace caret rule system url with an alias when the system definition is not known and alias is false', () => { + const valueset = new ExportableValueSet('MyValueSet'); + const rule = new ExportableCaretValueRule(''); + rule.caretPath = 'designation.use.display'; + rule.pathArray = ['http://example.org/tests/CodeSystem/mystery.codesystem#toast']; + rule.value = 'Complete bread classification'; + valueset.rules.push(rule); + const myPackage = new Package(); + myPackage.add(valueset); + optimizer.optimize(myPackage, fisher, { alias: false }); + + expect(rule.pathArray).toEqual([ + 'http://example.org/tests/CodeSystem/mystery.codesystem#toast' + ]); + }); + }); +}); diff --git a/test/optimizer/plugins/ResolveValueSetComponentRuleURLsOptimizer.test.ts b/test/optimizer/plugins/ResolveValueSetComponentRuleURLsOptimizer.test.ts index 1907905a..841513e3 100644 --- a/test/optimizer/plugins/ResolveValueSetComponentRuleURLsOptimizer.test.ts +++ b/test/optimizer/plugins/ResolveValueSetComponentRuleURLsOptimizer.test.ts @@ -194,7 +194,7 @@ describe('optimizer', () => { expect(valueset.rules).toContainEqual(expectedRule); }); - it('should alias the filter rule system url when it is same as local code system name when alias is true', () => { + it('should alias the filter rule valueset url when it is same as local code system name when alias is true', () => { const valueset = new ExportableValueSet('MyValueSet'); const rule = new ExportableValueSetFilterComponentRule(true); rule.from = { valueSets: ['http://hl7.org/fhir/ValueSet/observation-status'] }; @@ -215,7 +215,7 @@ describe('optimizer', () => { ]); }); - it('should alias the filter rule system url when it is same as local code system name when alias is undefined', () => { + it('should alias the filter rule valueset url when it is same as local code system name when alias is undefined', () => { const valueset = new ExportableValueSet('MyValueSet'); const rule = new ExportableValueSetFilterComponentRule(true); rule.from = { valueSets: ['http://hl7.org/fhir/ValueSet/observation-status'] }; diff --git a/test/optimizer/plugins/SeparateConceptsWithCaretRulesOptimizer.test.ts b/test/optimizer/plugins/SeparateConceptsWithCaretRulesOptimizer.test.ts new file mode 100644 index 00000000..6dec80ab --- /dev/null +++ b/test/optimizer/plugins/SeparateConceptsWithCaretRulesOptimizer.test.ts @@ -0,0 +1,102 @@ +import { fshtypes } from 'fsh-sushi'; +import { + ExportableCaretValueRule, + ExportableValueSet, + ExportableValueSetConceptComponentRule +} from '../../../src/exportable'; +import optimizer from '../../../src/optimizer/plugins/SeparateConceptsWithCaretRulesOptimizer'; +import { Package } from '../../../src/processor'; + +describe('optimizer', () => { + describe('#separate_concepts_with_caret_rules', () => { + it('should have appropriate metadata', () => { + expect(optimizer.name).toBe('separate_concepts_with_caret_rules'); + expect(optimizer.description).toBeDefined(); + expect(optimizer.runBefore).toEqual(['resolve_value_set_component_rule_urls']); + expect(optimizer.runAfter).toBeUndefined(); + }); + + it('should group concept caret rules with their concept when there are rules for more than one concept from the same system', () => { + const valueSet = new ExportableValueSet('MyValueSet'); + const zooConcepts = new ExportableValueSetConceptComponentRule(true); + zooConcepts.from.system = 'http://example.org/zoo'; + zooConcepts.concepts = [ + new fshtypes.FshCode('tiger', 'http://example.org/zoo'), + new fshtypes.FshCode('bear', 'http://example.org/zoo'), + new fshtypes.FshCode('tarantula', 'http://example.org/zoo'), + new fshtypes.FshCode('pelican', 'http://example.org/zoo'), + new fshtypes.FshCode('hippo', 'http://example.org/zoo') + ]; + + const bearDesignationValue = new ExportableCaretValueRule(''); + bearDesignationValue.isCodeCaretRule = true; + bearDesignationValue.pathArray = ['http://example.org/zoo#bear']; + bearDesignationValue.caretPath = 'designation.value'; + bearDesignationValue.value = 'ourse'; + + const bearDesignationLanguage = new ExportableCaretValueRule(''); + bearDesignationLanguage.isCodeCaretRule = true; + bearDesignationLanguage.pathArray = ['http://example.org/zoo#bear']; + bearDesignationLanguage.caretPath = 'designation.language'; + bearDesignationLanguage.value = new fshtypes.FshCode('fr'); + + const pelicanDesignationValue = new ExportableCaretValueRule(''); + pelicanDesignationValue.isCodeCaretRule = true; + pelicanDesignationValue.pathArray = ['http://example.org/zoo#pelican']; + pelicanDesignationValue.caretPath = 'designation.value'; + pelicanDesignationValue.value = 'pelícano'; + + const pelicanDesignationLanguage = new ExportableCaretValueRule(''); + pelicanDesignationLanguage.isCodeCaretRule = true; + pelicanDesignationLanguage.pathArray = ['http://example.org/zoo#pelican']; + pelicanDesignationLanguage.caretPath = 'designation.language'; + pelicanDesignationLanguage.value = new fshtypes.FshCode('es'); + + valueSet.rules = [ + zooConcepts, + bearDesignationValue, + bearDesignationLanguage, + pelicanDesignationValue, + pelicanDesignationLanguage + ]; + + const myPackage = new Package(); + myPackage.add(valueSet); + optimizer.optimize(myPackage); + + const expectedTigerConcept = new ExportableValueSetConceptComponentRule(true); + expectedTigerConcept.from.system = 'http://example.org/zoo'; + expectedTigerConcept.concepts = [new fshtypes.FshCode('tiger', 'http://example.org/zoo')]; + + const expectedBearConcept = new ExportableValueSetConceptComponentRule(true); + expectedBearConcept.from.system = 'http://example.org/zoo'; + expectedBearConcept.concepts = [new fshtypes.FshCode('bear', 'http://example.org/zoo')]; + + const expectedTarantulaConcept = new ExportableValueSetConceptComponentRule(true); + expectedTarantulaConcept.from.system = 'http://example.org/zoo'; + expectedTarantulaConcept.concepts = [ + new fshtypes.FshCode('tarantula', 'http://example.org/zoo') + ]; + + const expectedPelicanConcept = new ExportableValueSetConceptComponentRule(true); + expectedPelicanConcept.from.system = 'http://example.org/zoo'; + expectedPelicanConcept.concepts = [new fshtypes.FshCode('pelican', 'http://example.org/zoo')]; + + const expectedHippoConcept = new ExportableValueSetConceptComponentRule(true); + expectedHippoConcept.from.system = 'http://example.org/zoo'; + expectedHippoConcept.concepts = [new fshtypes.FshCode('hippo', 'http://example.org/zoo')]; + + expect(valueSet.rules).toEqual([ + expectedTigerConcept, + expectedBearConcept, + bearDesignationValue, + bearDesignationLanguage, + expectedTarantulaConcept, + expectedPelicanConcept, + pelicanDesignationValue, + pelicanDesignationLanguage, + expectedHippoConcept + ]); + }); + }); +}); diff --git a/test/optimizer/plugins/fixtures/unsupported-valueset.json b/test/optimizer/plugins/fixtures/unsupported-valueset.json index b7107f3d..5ded5735 100644 --- a/test/optimizer/plugins/fixtures/unsupported-valueset.json +++ b/test/optimizer/plugins/fixtures/unsupported-valueset.json @@ -4,11 +4,19 @@ "id": "unsupported.valueset", "url": "http://example.org/tests/ValueSet/unsupported.valueset", "title": "Unsupported ValueSet", - "description": "This value set is not supported by ValueSet FSH syntax because it has a concept designation.", + "description": "This value set is not supported by ValueSet FSH syntax because it has a version extension.", "compose": { "include": [ { "system": "http://example.org/zoo", + "_version": { + "extension": [ + { + "url": "http://example.org/SomeExtension", + "valueString": "version things" + } + ] + }, "concept": [ { "code": "BEAR", diff --git a/test/processor/CodeSystemProcessor.test.ts b/test/processor/CodeSystemProcessor.test.ts index fb2b2373..513dba23 100644 --- a/test/processor/CodeSystemProcessor.test.ts +++ b/test/processor/CodeSystemProcessor.test.ts @@ -89,13 +89,13 @@ describe('CodeSystemProcessor', () => { const caretRule1 = new ExportableCaretValueRule(''); caretRule1.caretPath = 'designation[0].language'; caretRule1.isCodeCaretRule = true; - caretRule1.pathArray = ['dangerous-dinner']; + caretRule1.pathArray = ['#dangerous-dinner']; caretRule1.value = new FshCode('fr'); const caretRule2 = new ExportableCaretValueRule(''); caretRule2.caretPath = 'designation[0].value'; caretRule2.isCodeCaretRule = true; - caretRule2.pathArray = ['dangerous-dinner']; + caretRule2.pathArray = ['#dangerous-dinner']; caretRule2.value = 'diner-dangereux'; expect(result).toBeInstanceOf(ExportableCodeSystem); @@ -114,13 +114,13 @@ describe('CodeSystemProcessor', () => { const caretRule1 = new ExportableCaretValueRule(''); caretRule1.caretPath = 'property[0].code'; caretRule1.isCodeCaretRule = true; - caretRule1.pathArray = ['breakfast']; + caretRule1.pathArray = ['#breakfast']; caretRule1.value = new FshCode('healthy'); const caretRule2 = new ExportableCaretValueRule(''); caretRule2.caretPath = 'property[0].valueCode'; caretRule2.isCodeCaretRule = true; - caretRule2.pathArray = ['breakfast']; + caretRule2.pathArray = ['#breakfast']; caretRule2.value = new FshCode('sometimes'); expect(result).toBeInstanceOf(ExportableCodeSystem); diff --git a/test/processor/ValueSetProcessor.test.ts b/test/processor/ValueSetProcessor.test.ts index ef4df3d6..44e1b186 100644 --- a/test/processor/ValueSetProcessor.test.ts +++ b/test/processor/ValueSetProcessor.test.ts @@ -66,7 +66,7 @@ describe('ValueSetProcessor', () => { ); }); - it('should not convert a ValueSet with an included concept designation', () => { + it('should convert a ValueSet with an included concept designation', () => { const input = JSON.parse( fs.readFileSync(path.join(__dirname, 'fixtures', 'composed-valueset.json'), 'utf-8') ); @@ -74,10 +74,10 @@ describe('ValueSetProcessor', () => { value: 'ourse' }; const result = ValueSetProcessor.process(input, defs, config); - expect(result).toBeUndefined(); + expect(result).toBeDefined(); }); - it('should not convert a ValueSet with an excluded concept designation', () => { + it('should convert a ValueSet with an excluded concept designation', () => { const input = JSON.parse( fs.readFileSync(path.join(__dirname, 'fixtures', 'composed-valueset.json'), 'utf-8') ); @@ -85,7 +85,7 @@ describe('ValueSetProcessor', () => { value: 'chatte' }; const result = ValueSetProcessor.process(input, defs, config); - expect(result).toBeUndefined(); + expect(result).toBeDefined(); }); it('should not convert a ValueSet with a compose.include id', () => { @@ -235,5 +235,69 @@ describe('ValueSetProcessor', () => { expect(targetValueSet.rules).toHaveLength(4); expect(targetValueSet.rules).toContainEqual(experimentalRule); }); + + it('should add concept caret rules to a ValueSet', () => { + const input = JSON.parse( + fs.readFileSync(path.join(__dirname, 'fixtures', 'composed-valueset.json'), 'utf-8') + ); + // add some designations + input.compose.include[0].concept[0].designation = { + value: 'ourse', + language: 'fr' + }; + input.compose.exclude[0].concept[0].designation = { + value: 'chatte', + language: 'fr' + }; + const workingValueSet = new ExportableValueSet('ComposedValueSet'); + ValueSetProcessor.extractRules(input, workingValueSet, defs, config); + const rules = workingValueSet.rules; + + const expectedIncludeDesignationValue = new ExportableCaretValueRule(''); + expectedIncludeDesignationValue.isCodeCaretRule = true; + expectedIncludeDesignationValue.pathArray = ['http://example.org/zoo#BEAR']; + expectedIncludeDesignationValue.caretPath = 'designation.value'; + expectedIncludeDesignationValue.value = 'ourse'; + expect(rules).toContainEqual(expectedIncludeDesignationValue); + + const expectedIncludeDesignationLanguage = new ExportableCaretValueRule(''); + expectedIncludeDesignationLanguage.isCodeCaretRule = true; + expectedIncludeDesignationLanguage.pathArray = ['http://example.org/zoo#BEAR']; + expectedIncludeDesignationLanguage.caretPath = 'designation.language'; + expectedIncludeDesignationLanguage.value = new FshCode('fr'); + expect(rules).toContainEqual(expectedIncludeDesignationLanguage); + + const expectedExcludeDesignationValue = new ExportableCaretValueRule(''); + expectedExcludeDesignationValue.isCodeCaretRule = true; + expectedExcludeDesignationValue.pathArray = ['http://example.org/zoo#CAT']; + expectedExcludeDesignationValue.caretPath = 'designation.value'; + expectedExcludeDesignationValue.value = 'chatte'; + expect(rules).toContainEqual(expectedExcludeDesignationValue); + + const expectedExcludeDesignationLanguage = new ExportableCaretValueRule(''); + expectedExcludeDesignationLanguage.isCodeCaretRule = true; + expectedExcludeDesignationLanguage.pathArray = ['http://example.org/zoo#CAT']; + expectedExcludeDesignationLanguage.caretPath = 'designation.language'; + expectedExcludeDesignationLanguage.value = new FshCode('fr'); + expect(rules).toContainEqual(expectedExcludeDesignationLanguage); + }); + + it('should log an error and not add a concept caret rule when the rule value is missing', () => { + const input = JSON.parse( + fs.readFileSync(path.join(__dirname, 'fixtures', 'composed-valueset.json'), 'utf-8') + ); + // add an empty designation + input.compose.include[0].concept[0].designation = {}; + + const workingValueSet = new ExportableValueSet('ComposedValueSet'); + ValueSetProcessor.extractRules(input, workingValueSet, defs, config); + const rules = workingValueSet.rules; + + expect(rules).not.toContainEqual(expect.objectContaining({ pathArray: ['BEAR'] })); + + expect(loggerSpy.getLastMessage('error')).toEqual( + 'Value in ValueSet ComposedValueSet at concept http://example.org/zoo#BEAR for element designation is empty. No caret value rule will be created.' + ); + }); }); }); diff --git a/test/processor/fixtures/unsupported-valueset-missing-id.json b/test/processor/fixtures/unsupported-valueset-missing-id.json index 45a89c50..d4f20826 100644 --- a/test/processor/fixtures/unsupported-valueset-missing-id.json +++ b/test/processor/fixtures/unsupported-valueset-missing-id.json @@ -2,11 +2,19 @@ "resourceType": "ValueSet", "name": "UnsupportedValueSet", "title": "Unsupported ValueSet", - "description": "This value set is not supported by ValueSet FSH syntax because it has a concept designation.", + "description": "This value set is not supported by ValueSet FSH syntax because it has a version extension.", "compose": { "include": [ { "system": "http://example.org/zoo", + "_version": { + "extension": [ + { + "url": "http://example.org/SomeExtension", + "valueString": "version things" + } + ] + }, "concept": [ { "code": "BEAR", diff --git a/test/processor/fixtures/unsupported-valueset.json b/test/processor/fixtures/unsupported-valueset.json index a2ebcc50..c9ad5b1b 100644 --- a/test/processor/fixtures/unsupported-valueset.json +++ b/test/processor/fixtures/unsupported-valueset.json @@ -3,11 +3,19 @@ "name": "UnsupportedValueSet", "id": "unsupported.valueset", "title": "Unsupported ValueSet", - "description": "This value set is not supported by ValueSet FSH syntax because it has a concept designation.", + "description": "This value set is not supported by ValueSet FSH syntax because it has a version extension.", "compose": { "include": [ { "system": "http://example.org/zoo", + "_version": { + "extension": [ + { + "url": "http://example.org/SomeExtension", + "valueString": "version things" + } + ] + }, "concept": [ { "code": "BEAR",