fhirpath-ts 0.0.0-bootstrap.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +391 -0
- package/THIRD-PARTY-NOTICES.md +296 -0
- package/dist/analyzer/analyze-dto.d.ts +45 -0
- package/dist/analyzer/analyze-dto.d.ts.map +1 -0
- package/dist/analyzer/analyze-dto.js +147 -0
- package/dist/analyzer/analyze-dto.js.map +1 -0
- package/dist/analyzer/analyze.d.ts +98 -0
- package/dist/analyzer/analyze.d.ts.map +1 -0
- package/dist/analyzer/analyze.js +925 -0
- package/dist/analyzer/analyze.js.map +1 -0
- package/dist/analyzer/expression-policy.d.ts +210 -0
- package/dist/analyzer/expression-policy.d.ts.map +1 -0
- package/dist/analyzer/expression-policy.js +254 -0
- package/dist/analyzer/expression-policy.js.map +1 -0
- package/dist/analyzer/index.d.ts +3 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +6 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/analyzer/regex-safety.d.ts +10 -0
- package/dist/analyzer/regex-safety.d.ts.map +1 -0
- package/dist/analyzer/regex-safety.js +84 -0
- package/dist/analyzer/regex-safety.js.map +1 -0
- package/dist/analyzer/signatures.d.ts +72 -0
- package/dist/analyzer/signatures.d.ts.map +1 -0
- package/dist/analyzer/signatures.js +195 -0
- package/dist/analyzer/signatures.js.map +1 -0
- package/dist/api/bundle.d.ts +29 -0
- package/dist/api/bundle.d.ts.map +1 -0
- package/dist/api/bundle.js +108 -0
- package/dist/api/bundle.js.map +1 -0
- package/dist/api/cache.d.ts +10 -0
- package/dist/api/cache.d.ts.map +1 -0
- package/dist/api/cache.js +38 -0
- package/dist/api/cache.js.map +1 -0
- package/dist/api/column-signature.d.ts +53 -0
- package/dist/api/column-signature.d.ts.map +1 -0
- package/dist/api/column-signature.js +43 -0
- package/dist/api/column-signature.js.map +1 -0
- package/dist/api/compile.d.ts +120 -0
- package/dist/api/compile.d.ts.map +1 -0
- package/dist/api/compile.js +167 -0
- package/dist/api/compile.js.map +1 -0
- package/dist/api/constraints.d.ts +48 -0
- package/dist/api/constraints.d.ts.map +1 -0
- package/dist/api/constraints.js +52 -0
- package/dist/api/constraints.js.map +1 -0
- package/dist/api/dto.d.ts +134 -0
- package/dist/api/dto.d.ts.map +1 -0
- package/dist/api/dto.js +295 -0
- package/dist/api/dto.js.map +1 -0
- package/dist/api/engine.d.ts +136 -0
- package/dist/api/engine.d.ts.map +1 -0
- package/dist/api/engine.js +183 -0
- package/dist/api/engine.js.map +1 -0
- package/dist/api/evaluate.d.ts +7 -0
- package/dist/api/evaluate.d.ts.map +1 -0
- package/dist/api/evaluate.js +10 -0
- package/dist/api/evaluate.js.map +1 -0
- package/dist/api/project.d.ts +89 -0
- package/dist/api/project.d.ts.map +1 -0
- package/dist/api/project.js +150 -0
- package/dist/api/project.js.map +1 -0
- package/dist/api/tagged.d.ts +14 -0
- package/dist/api/tagged.d.ts.map +1 -0
- package/dist/api/tagged.js +13 -0
- package/dist/api/tagged.js.map +1 -0
- package/dist/cli/dto-check.d.ts +25 -0
- package/dist/cli/dto-check.d.ts.map +1 -0
- package/dist/cli/dto-check.js +77 -0
- package/dist/cli/dto-check.js.map +1 -0
- package/dist/cli/fhirpath-check.d.ts +3 -0
- package/dist/cli/fhirpath-check.d.ts.map +1 -0
- package/dist/cli/fhirpath-check.js +149 -0
- package/dist/cli/fhirpath-check.js.map +1 -0
- package/dist/cli/ts-loader.d.mts +3 -0
- package/dist/cli/ts-loader.d.mts.map +1 -0
- package/dist/cli/ts-loader.mjs +48 -0
- package/dist/cli/ts-loader.mjs.map +1 -0
- package/dist/engine/context.d.ts +173 -0
- package/dist/engine/context.d.ts.map +1 -0
- package/dist/engine/context.js +118 -0
- package/dist/engine/context.js.map +1 -0
- package/dist/engine/evaluator.d.ts +7 -0
- package/dist/engine/evaluator.d.ts.map +1 -0
- package/dist/engine/evaluator.js +196 -0
- package/dist/engine/evaluator.js.map +1 -0
- package/dist/engine/navigation.d.ts +11 -0
- package/dist/engine/navigation.d.ts.map +1 -0
- package/dist/engine/navigation.js +102 -0
- package/dist/engine/navigation.js.map +1 -0
- package/dist/engine/operators/collections.d.ts +12 -0
- package/dist/engine/operators/collections.d.ts.map +1 -0
- package/dist/engine/operators/collections.js +31 -0
- package/dist/engine/operators/collections.js.map +1 -0
- package/dist/engine/operators/comparison.d.ts +13 -0
- package/dist/engine/operators/comparison.d.ts.map +1 -0
- package/dist/engine/operators/comparison.js +77 -0
- package/dist/engine/operators/comparison.js.map +1 -0
- package/dist/engine/operators/equality.d.ts +19 -0
- package/dist/engine/operators/equality.d.ts.map +1 -0
- package/dist/engine/operators/equality.js +203 -0
- package/dist/engine/operators/equality.js.map +1 -0
- package/dist/engine/operators/index.d.ts +18 -0
- package/dist/engine/operators/index.d.ts.map +1 -0
- package/dist/engine/operators/index.js +30 -0
- package/dist/engine/operators/index.js.map +1 -0
- package/dist/engine/operators/logic.d.ts +8 -0
- package/dist/engine/operators/logic.d.ts.map +1 -0
- package/dist/engine/operators/logic.js +38 -0
- package/dist/engine/operators/logic.js.map +1 -0
- package/dist/engine/operators/math.d.ts +13 -0
- package/dist/engine/operators/math.d.ts.map +1 -0
- package/dist/engine/operators/math.js +198 -0
- package/dist/engine/operators/math.js.map +1 -0
- package/dist/engine/operators/strings.d.ts +8 -0
- package/dist/engine/operators/strings.d.ts.map +1 -0
- package/dist/engine/operators/strings.js +22 -0
- package/dist/engine/operators/strings.js.map +1 -0
- package/dist/engine/operators/types.d.ts +3 -0
- package/dist/engine/operators/types.d.ts.map +1 -0
- package/dist/engine/operators/types.js +15 -0
- package/dist/engine/operators/types.js.map +1 -0
- package/dist/engine/type-matching.d.ts +20 -0
- package/dist/engine/type-matching.d.ts.map +1 -0
- package/dist/engine/type-matching.js +122 -0
- package/dist/engine/type-matching.js.map +1 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +21 -0
- package/dist/errors.js.map +1 -0
- package/dist/eslint/index.d.ts +12 -0
- package/dist/eslint/index.d.ts.map +1 -0
- package/dist/eslint/index.js +316 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/fhir/html-checks.d.ts +7 -0
- package/dist/fhir/html-checks.d.ts.map +1 -0
- package/dist/fhir/html-checks.js +256 -0
- package/dist/fhir/html-checks.js.map +1 -0
- package/dist/fhir/model-navigation.d.ts +11 -0
- package/dist/fhir/model-navigation.d.ts.map +1 -0
- package/dist/fhir/model-navigation.js +142 -0
- package/dist/fhir/model-navigation.js.map +1 -0
- package/dist/functions/aggregate.d.ts +2 -0
- package/dist/functions/aggregate.d.ts.map +1 -0
- package/dist/functions/aggregate.js +127 -0
- package/dist/functions/aggregate.js.map +1 -0
- package/dist/functions/boundary.d.ts +2 -0
- package/dist/functions/boundary.d.ts.map +1 -0
- package/dist/functions/boundary.js +189 -0
- package/dist/functions/boundary.js.map +1 -0
- package/dist/functions/combining.d.ts +2 -0
- package/dist/functions/combining.d.ts.map +1 -0
- package/dist/functions/combining.js +13 -0
- package/dist/functions/combining.js.map +1 -0
- package/dist/functions/control.d.ts +2 -0
- package/dist/functions/control.d.ts.map +1 -0
- package/dist/functions/control.js +25 -0
- package/dist/functions/control.js.map +1 -0
- package/dist/functions/conversion.d.ts +3 -0
- package/dist/functions/conversion.d.ts.map +1 -0
- package/dist/functions/conversion.js +316 -0
- package/dist/functions/conversion.js.map +1 -0
- package/dist/functions/date-components.d.ts +2 -0
- package/dist/functions/date-components.d.ts.map +1 -0
- package/dist/functions/date-components.js +81 -0
- package/dist/functions/date-components.js.map +1 -0
- package/dist/functions/existence.d.ts +4 -0
- package/dist/functions/existence.d.ts.map +1 -0
- package/dist/functions/existence.js +97 -0
- package/dist/functions/existence.js.map +1 -0
- package/dist/functions/fhir.d.ts +2 -0
- package/dist/functions/fhir.d.ts.map +1 -0
- package/dist/functions/fhir.js +222 -0
- package/dist/functions/fhir.js.map +1 -0
- package/dist/functions/filtering.d.ts +2 -0
- package/dist/functions/filtering.d.ts.map +1 -0
- package/dist/functions/filtering.js +112 -0
- package/dist/functions/filtering.js.map +1 -0
- package/dist/functions/install.d.ts +18 -0
- package/dist/functions/install.d.ts.map +1 -0
- package/dist/functions/install.js +20 -0
- package/dist/functions/install.js.map +1 -0
- package/dist/functions/iteration.d.ts +7 -0
- package/dist/functions/iteration.d.ts.map +1 -0
- package/dist/functions/iteration.js +9 -0
- package/dist/functions/iteration.js.map +1 -0
- package/dist/functions/math.d.ts +2 -0
- package/dist/functions/math.d.ts.map +1 -0
- package/dist/functions/math.js +152 -0
- package/dist/functions/math.js.map +1 -0
- package/dist/functions/reflection.d.ts +2 -0
- package/dist/functions/reflection.d.ts.map +1 -0
- package/dist/functions/reflection.js +23 -0
- package/dist/functions/reflection.js.map +1 -0
- package/dist/functions/registry.d.ts +28 -0
- package/dist/functions/registry.d.ts.map +1 -0
- package/dist/functions/registry.js +46 -0
- package/dist/functions/registry.js.map +1 -0
- package/dist/functions/sort.d.ts +2 -0
- package/dist/functions/sort.d.ts.map +1 -0
- package/dist/functions/sort.js +48 -0
- package/dist/functions/sort.js.map +1 -0
- package/dist/functions/string.d.ts +2 -0
- package/dist/functions/string.d.ts.map +1 -0
- package/dist/functions/string.js +353 -0
- package/dist/functions/string.js.map +1 -0
- package/dist/functions/subsetting.d.ts +2 -0
- package/dist/functions/subsetting.d.ts.map +1 -0
- package/dist/functions/subsetting.js +72 -0
- package/dist/functions/subsetting.js.map +1 -0
- package/dist/functions/tree.d.ts +8 -0
- package/dist/functions/tree.d.ts.map +1 -0
- package/dist/functions/tree.js +109 -0
- package/dist/functions/tree.js.map +1 -0
- package/dist/functions/type-specifier.d.ts +8 -0
- package/dist/functions/type-specifier.d.ts.map +1 -0
- package/dist/functions/type-specifier.js +27 -0
- package/dist/functions/type-specifier.js.map +1 -0
- package/dist/functions/utility.d.ts +2 -0
- package/dist/functions/utility.d.ts.map +1 -0
- package/dist/functions/utility.js +76 -0
- package/dist/functions/utility.js.map +1 -0
- package/dist/functions/variables.d.ts +2 -0
- package/dist/functions/variables.d.ts.map +1 -0
- package/dist/functions/variables.js +35 -0
- package/dist/functions/variables.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer/lexer.d.ts +4 -0
- package/dist/lexer/lexer.d.ts.map +1 -0
- package/dist/lexer/lexer.js +318 -0
- package/dist/lexer/lexer.js.map +1 -0
- package/dist/lexer/tokens.d.ts +23 -0
- package/dist/lexer/tokens.d.ts.map +1 -0
- package/dist/lexer/tokens.js +40 -0
- package/dist/lexer/tokens.js.map +1 -0
- package/dist/model/provider.d.ts +36 -0
- package/dist/model/provider.d.ts.map +1 -0
- package/dist/model/provider.js +2 -0
- package/dist/model/provider.js.map +1 -0
- package/dist/parser/ast.d.ts +104 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/ast.js +2 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/parser.d.ts +4 -0
- package/dist/parser/parser.d.ts.map +1 -0
- package/dist/parser/parser.js +299 -0
- package/dist/parser/parser.js.map +1 -0
- package/dist/parser/precedence.d.ts +24 -0
- package/dist/parser/precedence.d.ts.map +1 -0
- package/dist/parser/precedence.js +52 -0
- package/dist/parser/precedence.js.map +1 -0
- package/dist/parser/printer.d.ts +4 -0
- package/dist/parser/printer.d.ts.map +1 -0
- package/dist/parser/printer.js +112 -0
- package/dist/parser/printer.js.map +1 -0
- package/dist/r4/generated/resources-data.d.ts +3 -0
- package/dist/r4/generated/resources-data.d.ts.map +1 -0
- package/dist/r4/generated/resources-data.js +28628 -0
- package/dist/r4/generated/resources-data.js.map +1 -0
- package/dist/r4/generated/type-maps.d.ts +43153 -0
- package/dist/r4/generated/type-maps.d.ts.map +1 -0
- package/dist/r4/generated/type-maps.js +4 -0
- package/dist/r4/generated/type-maps.js.map +1 -0
- package/dist/r4/generated/types-data.d.ts +3 -0
- package/dist/r4/generated/types-data.d.ts.map +1 -0
- package/dist/r4/generated/types-data.js +2205 -0
- package/dist/r4/generated/types-data.js.map +1 -0
- package/dist/r4/index.d.ts +9 -0
- package/dist/r4/index.d.ts.map +1 -0
- package/dist/r4/index.js +94 -0
- package/dist/r4/index.js.map +1 -0
- package/dist/r4/model-data.d.ts +17 -0
- package/dist/r4/model-data.d.ts.map +1 -0
- package/dist/r4/model-data.js +2 -0
- package/dist/r4/model-data.js.map +1 -0
- package/dist/sites/index.d.ts +31 -0
- package/dist/sites/index.d.ts.map +1 -0
- package/dist/sites/index.js +205 -0
- package/dist/sites/index.js.map +1 -0
- package/dist/typed/infer.d.ts +234 -0
- package/dist/typed/infer.d.ts.map +1 -0
- package/dist/typed/infer.js +85 -0
- package/dist/typed/infer.js.map +1 -0
- package/dist/typed/perf-fixture.types.d.ts +136 -0
- package/dist/typed/perf-fixture.types.d.ts.map +1 -0
- package/dist/typed/perf-fixture.types.js +2 -0
- package/dist/typed/perf-fixture.types.js.map +1 -0
- package/dist/values/collection.d.ts +24 -0
- package/dist/values/collection.d.ts.map +1 -0
- package/dist/values/collection.js +51 -0
- package/dist/values/collection.js.map +1 -0
- package/dist/values/datetime.d.ts +54 -0
- package/dist/values/datetime.d.ts.map +1 -0
- package/dist/values/datetime.js +226 -0
- package/dist/values/datetime.js.map +1 -0
- package/dist/values/decimal.d.ts +48 -0
- package/dist/values/decimal.d.ts.map +1 -0
- package/dist/values/decimal.js +188 -0
- package/dist/values/decimal.js.map +1 -0
- package/dist/values/numeric.d.ts +24 -0
- package/dist/values/numeric.d.ts.map +1 -0
- package/dist/values/numeric.js +53 -0
- package/dist/values/numeric.js.map +1 -0
- package/dist/values/quantity.d.ts +49 -0
- package/dist/values/quantity.d.ts.map +1 -0
- package/dist/values/quantity.js +297 -0
- package/dist/values/quantity.js.map +1 -0
- package/dist/values/temporal-arithmetic.d.ts +17 -0
- package/dist/values/temporal-arithmetic.d.ts.map +1 -0
- package/dist/values/temporal-arithmetic.js +183 -0
- package/dist/values/temporal-arithmetic.js.map +1 -0
- package/dist/values/temporal-compare.d.ts +9 -0
- package/dist/values/temporal-compare.d.ts.map +1 -0
- package/dist/values/temporal-compare.js +59 -0
- package/dist/values/temporal-compare.js.map +1 -0
- package/dist/values/type-compat.d.ts +47 -0
- package/dist/values/type-compat.d.ts.map +1 -0
- package/dist/values/type-compat.js +100 -0
- package/dist/values/type-compat.js.map +1 -0
- package/dist/values/typed-value.d.ts +48 -0
- package/dist/values/typed-value.d.ts.map +1 -0
- package/dist/values/typed-value.js +117 -0
- package/dist/values/typed-value.js.map +1 -0
- package/dist/values/ucum.d.ts +14 -0
- package/dist/values/ucum.d.ts.map +1 -0
- package/dist/values/ucum.js +146 -0
- package/dist/values/ucum.js.map +1 -0
- package/package.json +137 -0
- package/src/analyzer/analyze-dto.ts +195 -0
- package/src/analyzer/analyze.ts +1181 -0
- package/src/analyzer/expression-policy.ts +434 -0
- package/src/analyzer/index.ts +23 -0
- package/src/analyzer/regex-safety.ts +85 -0
- package/src/analyzer/signatures.ts +260 -0
- package/src/api/bundle.ts +132 -0
- package/src/api/cache.ts +40 -0
- package/src/api/column-signature.ts +71 -0
- package/src/api/compile.ts +308 -0
- package/src/api/constraints.ts +104 -0
- package/src/api/dto.ts +445 -0
- package/src/api/engine.ts +328 -0
- package/src/api/evaluate.ts +11 -0
- package/src/api/project.ts +255 -0
- package/src/api/tagged.ts +35 -0
- package/src/cli/dto-check.ts +94 -0
- package/src/cli/fhirpath-check.ts +168 -0
- package/src/cli/ts-loader.mjs +49 -0
- package/src/engine/context.ts +282 -0
- package/src/engine/evaluator.ts +246 -0
- package/src/engine/navigation.ts +106 -0
- package/src/engine/operators/collections.ts +34 -0
- package/src/engine/operators/comparison.ts +82 -0
- package/src/engine/operators/equality.ts +232 -0
- package/src/engine/operators/index.ts +65 -0
- package/src/engine/operators/logic.ts +53 -0
- package/src/engine/operators/math.ts +225 -0
- package/src/engine/operators/strings.ts +25 -0
- package/src/engine/operators/types.ts +16 -0
- package/src/engine/type-matching.ts +148 -0
- package/src/errors.ts +33 -0
- package/src/eslint/index.ts +388 -0
- package/src/fhir/html-checks.ts +266 -0
- package/src/fhir/model-navigation.ts +148 -0
- package/src/functions/aggregate.ts +136 -0
- package/src/functions/boundary.ts +209 -0
- package/src/functions/combining.ts +15 -0
- package/src/functions/control.ts +25 -0
- package/src/functions/conversion.ts +349 -0
- package/src/functions/date-components.ts +88 -0
- package/src/functions/existence.ts +107 -0
- package/src/functions/fhir.ts +247 -0
- package/src/functions/filtering.ts +120 -0
- package/src/functions/install.ts +19 -0
- package/src/functions/iteration.ts +21 -0
- package/src/functions/math.ts +167 -0
- package/src/functions/reflection.ts +24 -0
- package/src/functions/registry.ts +72 -0
- package/src/functions/sort.ts +68 -0
- package/src/functions/string.ts +413 -0
- package/src/functions/subsetting.ts +89 -0
- package/src/functions/tree.ts +112 -0
- package/src/functions/type-specifier.ts +29 -0
- package/src/functions/utility.ts +82 -0
- package/src/functions/variables.ts +36 -0
- package/src/index.ts +30 -0
- package/src/lexer/lexer.ts +341 -0
- package/src/lexer/tokens.ts +69 -0
- package/src/model/provider.ts +36 -0
- package/src/parser/ast.ts +164 -0
- package/src/parser/parser.ts +321 -0
- package/src/parser/precedence.ts +52 -0
- package/src/parser/printer.ts +114 -0
- package/src/r4/generated/resources-data.ts +28631 -0
- package/src/r4/generated/type-maps.ts +22145 -0
- package/src/r4/generated/types-data.ts +2208 -0
- package/src/r4/index.ts +107 -0
- package/src/r4/model-data.ts +17 -0
- package/src/sites/index.ts +285 -0
- package/src/typed/infer.ts +433 -0
- package/src/typed/perf-fixture.types.ts +143 -0
- package/src/values/collection.ts +57 -0
- package/src/values/datetime.ts +285 -0
- package/src/values/decimal.ts +213 -0
- package/src/values/numeric.ts +63 -0
- package/src/values/quantity.ts +358 -0
- package/src/values/temporal-arithmetic.ts +231 -0
- package/src/values/temporal-compare.ts +65 -0
- package/src/values/type-compat.ts +133 -0
- package/src/values/typed-value.ts +151 -0
- package/src/values/ucum.ts +181 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type TypedValue, typeLocalName } from '../values/typed-value.ts'
|
|
2
|
+
import { registerFunction } from './registry.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* type() reflection (spec §10.2, STU). Returns one TypeInfo per item; the official
|
|
6
|
+
* tests only inspect `namespace` and `name`, so ClassInfo element lists are not
|
|
7
|
+
* materialized.
|
|
8
|
+
*/
|
|
9
|
+
registerFunction('type', {
|
|
10
|
+
minArity: 0,
|
|
11
|
+
maxArity: 0,
|
|
12
|
+
evaluate: (context, input) => input.map(item => typeInfoOf(item, context.model?.namespace)),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
function typeInfoOf(item: TypedValue, modelNamespace: string | undefined): TypedValue {
|
|
16
|
+
const separator = item.type.lastIndexOf('.')
|
|
17
|
+
const namespace = separator === -1 ? (modelNamespace ?? 'FHIR') : item.type.slice(0, separator)
|
|
18
|
+
const name = typeLocalName(item.type)
|
|
19
|
+
const isSystem = namespace === 'System'
|
|
20
|
+
return {
|
|
21
|
+
type: isSystem ? 'System.SimpleTypeInfo' : 'System.ClassInfo',
|
|
22
|
+
value: { namespace, name },
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { EvaluationContext } from '../engine/context.ts'
|
|
2
|
+
import { FhirPathTypeError } from '../errors.ts'
|
|
3
|
+
import type { AstNode } from '../parser/ast.ts'
|
|
4
|
+
import type { TypedValue } from '../values/typed-value.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A FHIRPath function. Arguments arrive as unevaluated ASTs so functions with
|
|
8
|
+
* expression parameters (`where`, `select`, `iif`, `aggregate`, ...) control their
|
|
9
|
+
* own scoping and laziness; plain-value functions evaluate them via the callback.
|
|
10
|
+
*/
|
|
11
|
+
export interface FhirPathFunction {
|
|
12
|
+
/** Inclusive argument count range. */
|
|
13
|
+
minArity: number
|
|
14
|
+
maxArity: number
|
|
15
|
+
evaluate(
|
|
16
|
+
context: EvaluationContext,
|
|
17
|
+
input: TypedValue[],
|
|
18
|
+
args: AstNode[],
|
|
19
|
+
evaluateNode: (node: AstNode, context: EvaluationContext, input: TypedValue[]) => TypedValue[]
|
|
20
|
+
): TypedValue[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The argument AST at `index`. `lookupFunction` has already enforced the arity
|
|
25
|
+
* range, so within a registered `evaluate` the node exists whenever `index` is
|
|
26
|
+
* below the function's minArity; this replaces the unchecked `args[i] as AstNode`
|
|
27
|
+
* casts with one guarded accessor.
|
|
28
|
+
*/
|
|
29
|
+
export function argAt(args: AstNode[], index: number): AstNode {
|
|
30
|
+
const node = args[index]
|
|
31
|
+
/* v8 ignore next 3 -- arity is validated before evaluate runs */
|
|
32
|
+
if (node === undefined) {
|
|
33
|
+
throw new FhirPathTypeError(`Missing required argument at position ${index}`)
|
|
34
|
+
}
|
|
35
|
+
return node
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Function table; the per-section function modules add entries (append-only). */
|
|
39
|
+
export const functions = new Map<string, FhirPathFunction>()
|
|
40
|
+
|
|
41
|
+
export function registerFunction(name: string, fn: FhirPathFunction): void {
|
|
42
|
+
if (functions.has(name)) {
|
|
43
|
+
// 100+ names across 20 modules: a copy-paste collision would silently
|
|
44
|
+
// replace an implementation, so fail loudly at import time instead.
|
|
45
|
+
throw new Error(`FHIRPath function '${name}' is registered twice`)
|
|
46
|
+
}
|
|
47
|
+
functions.set(name, fn)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function lookupFunction(name: string, argCount: number): FhirPathFunction {
|
|
51
|
+
const fn = functions.get(name)
|
|
52
|
+
if (!fn) {
|
|
53
|
+
throw new FhirPathTypeError(`Unrecognized function '${name}'`)
|
|
54
|
+
}
|
|
55
|
+
if (argCount < fn.minArity || argCount > fn.maxArity) {
|
|
56
|
+
throw new FhirPathTypeError(
|
|
57
|
+
`Function '${name}' expects ${describeArity(fn.minArity, fn.maxArity)}, got ${argCount} arguments`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
return fn
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Human-readable arity range, e.g. "1 argument", "2 arguments", "1 to 3 arguments". */
|
|
64
|
+
export function describeArity(minArity: number, maxArity: number): string {
|
|
65
|
+
if (minArity === maxArity) {
|
|
66
|
+
return minArity === 1 ? '1 argument' : `${minArity} arguments`
|
|
67
|
+
}
|
|
68
|
+
if (maxArity === Number.POSITIVE_INFINITY) {
|
|
69
|
+
return minArity === 1 ? 'at least 1 argument' : `at least ${minArity} arguments`
|
|
70
|
+
}
|
|
71
|
+
return `${minArity} to ${maxArity} arguments`
|
|
72
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { EvaluationContext } from '../engine/context.ts'
|
|
2
|
+
import { withFrame } from '../engine/context.ts'
|
|
3
|
+
import { compareValues } from '../engine/operators/comparison.ts'
|
|
4
|
+
import type { AstNode } from '../parser/ast.ts'
|
|
5
|
+
import { singleton } from '../values/collection.ts'
|
|
6
|
+
import type { TypedValue } from '../values/typed-value.ts'
|
|
7
|
+
import type { NodeEvaluator } from './iteration.ts'
|
|
8
|
+
import { registerFunction } from './registry.ts'
|
|
9
|
+
|
|
10
|
+
interface SortKey {
|
|
11
|
+
expression: AstNode | undefined
|
|
12
|
+
descending: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** sort([keys...]) — ballot STU. `-key` sorts that key descending; no keys sorts by value. */
|
|
16
|
+
registerFunction('sort', {
|
|
17
|
+
minArity: 0,
|
|
18
|
+
// The ballot puts no limit on sort keys; 8 is a practical cap for arity checking.
|
|
19
|
+
maxArity: 8,
|
|
20
|
+
evaluate: (context, input, args, evaluateNode) => {
|
|
21
|
+
const keys: SortKey[] = args.length
|
|
22
|
+
? args.map(node =>
|
|
23
|
+
node.kind === 'unary' && node.operator === '-'
|
|
24
|
+
? { expression: node.operand, descending: true }
|
|
25
|
+
: { expression: node, descending: false }
|
|
26
|
+
)
|
|
27
|
+
: [{ expression: undefined, descending: false }]
|
|
28
|
+
const decorated = input.map((item, index) => ({
|
|
29
|
+
item,
|
|
30
|
+
index,
|
|
31
|
+
values: keys.map(key => keyValue(context, item, key.expression, evaluateNode)),
|
|
32
|
+
}))
|
|
33
|
+
decorated.sort((a, b) => {
|
|
34
|
+
for (let i = 0; i < keys.length; i++) {
|
|
35
|
+
const comparison = compareKey(a.values[i], b.values[i], (keys[i] as SortKey).descending)
|
|
36
|
+
if (comparison !== 0) {
|
|
37
|
+
return comparison
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return a.index - b.index
|
|
41
|
+
})
|
|
42
|
+
return decorated.map(entry => entry.item)
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function keyValue(
|
|
47
|
+
context: EvaluationContext,
|
|
48
|
+
item: TypedValue,
|
|
49
|
+
expression: AstNode | undefined,
|
|
50
|
+
evaluateNode: NodeEvaluator
|
|
51
|
+
): TypedValue | undefined {
|
|
52
|
+
if (expression === undefined) {
|
|
53
|
+
return item
|
|
54
|
+
}
|
|
55
|
+
return withFrame(context, { thisValue: [item] }, frameContext =>
|
|
56
|
+
singleton(evaluateNode(expression, frameContext, [item]))
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compareKey(a: TypedValue | undefined, b: TypedValue | undefined, descending: boolean): number {
|
|
61
|
+
// Empty keys sort after present ones (ascending); descending reverses everything.
|
|
62
|
+
if (a === undefined || b === undefined) {
|
|
63
|
+
const emptiness = (a === undefined ? 1 : 0) - (b === undefined ? 1 : 0)
|
|
64
|
+
return descending ? -emptiness : emptiness
|
|
65
|
+
}
|
|
66
|
+
const comparison = compareValues(a, b) ?? 0
|
|
67
|
+
return descending ? -comparison : comparison
|
|
68
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import type { EvaluationContext } from '../engine/context.ts'
|
|
2
|
+
import { FhirPathTypeError } from '../errors.ts'
|
|
3
|
+
import type { AstNode } from '../parser/ast.ts'
|
|
4
|
+
import { singleton, wrapBoolean } from '../values/collection.ts'
|
|
5
|
+
import { SYSTEM_INTEGER, SYSTEM_STRING, systemTypeOf, type TypedValue } from '../values/typed-value.ts'
|
|
6
|
+
import type { NodeEvaluator } from './iteration.ts'
|
|
7
|
+
import { argAt, type FhirPathFunction, registerFunction } from './registry.ts'
|
|
8
|
+
|
|
9
|
+
/** Singleton String input; empty stays empty, anything else is a type error (spec §5.6). */
|
|
10
|
+
function stringInput(name: string, input: TypedValue[]): string | undefined {
|
|
11
|
+
const item = singleton(input)
|
|
12
|
+
if (item === undefined) {
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
|
15
|
+
if (systemTypeOf(item) !== SYSTEM_STRING) {
|
|
16
|
+
throw new FhirPathTypeError(`${name}() expects a String input, found ${item.type}`)
|
|
17
|
+
}
|
|
18
|
+
return item.value as string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stringArgument(
|
|
22
|
+
name: string,
|
|
23
|
+
context: EvaluationContext,
|
|
24
|
+
input: TypedValue[],
|
|
25
|
+
node: AstNode,
|
|
26
|
+
evaluateNode: NodeEvaluator
|
|
27
|
+
): string | undefined {
|
|
28
|
+
const item = singleton(evaluateNode(node, context, input))
|
|
29
|
+
if (item === undefined) {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
if (systemTypeOf(item) !== SYSTEM_STRING) {
|
|
33
|
+
throw new FhirPathTypeError(`${name}() expects a String argument, found ${item.type}`)
|
|
34
|
+
}
|
|
35
|
+
return item.value as string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const str = (value: string): TypedValue[] => [{ type: SYSTEM_STRING, value }]
|
|
39
|
+
const int = (value: number): TypedValue[] => [{ type: SYSTEM_INTEGER, value }]
|
|
40
|
+
|
|
41
|
+
/** Register a function over a singleton String input; empty input propagates. */
|
|
42
|
+
function stringFunction(
|
|
43
|
+
name: string,
|
|
44
|
+
arity: { min: number; max: number },
|
|
45
|
+
body: (value: string, args: (string | undefined)[], context: EvaluationContext) => TypedValue[]
|
|
46
|
+
): void {
|
|
47
|
+
const fn: FhirPathFunction = {
|
|
48
|
+
minArity: arity.min,
|
|
49
|
+
maxArity: arity.max,
|
|
50
|
+
evaluate: (context, input, args, evaluateNode) => {
|
|
51
|
+
const value = stringInput(name, input)
|
|
52
|
+
if (value === undefined) {
|
|
53
|
+
return []
|
|
54
|
+
}
|
|
55
|
+
const argValues = args.map(arg => stringArgument(name, context, input, arg, evaluateNode))
|
|
56
|
+
return body(value, argValues, context)
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
registerFunction(name, fn)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
stringFunction('indexOf', { min: 1, max: 1 }, (value, [substring]) =>
|
|
63
|
+
substring === undefined ? [] : int(value.indexOf(substring))
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
stringFunction('lastIndexOf', { min: 1, max: 1 }, (value, [substring]) =>
|
|
67
|
+
substring === undefined ? [] : int(value.lastIndexOf(substring))
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
registerFunction('substring', {
|
|
71
|
+
minArity: 1,
|
|
72
|
+
maxArity: 2,
|
|
73
|
+
evaluate: (context, input, args, evaluateNode) => {
|
|
74
|
+
const value = stringInput('substring', input)
|
|
75
|
+
if (value === undefined) {
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
const start = singleton(evaluateNode(argAt(args, 0), context, input), SYSTEM_INTEGER)
|
|
79
|
+
if (start === undefined || typeof start.value !== 'number') {
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
if (start.value < 0 || start.value >= value.length) {
|
|
83
|
+
return []
|
|
84
|
+
}
|
|
85
|
+
if (args.length === 1) {
|
|
86
|
+
return str(value.slice(start.value))
|
|
87
|
+
}
|
|
88
|
+
const lengthCollection = evaluateNode(argAt(args, 1), context, input)
|
|
89
|
+
if (lengthCollection.length === 0) {
|
|
90
|
+
// An empty length means "to the end", like the one-argument form.
|
|
91
|
+
return str(value.slice(start.value))
|
|
92
|
+
}
|
|
93
|
+
const length = singleton(lengthCollection, SYSTEM_INTEGER)
|
|
94
|
+
/* v8 ignore next 3 -- singleton(SYSTEM_INTEGER) only returns integer items */
|
|
95
|
+
if (length === undefined || typeof length.value !== 'number') {
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
return str(value.slice(start.value, start.value + Math.max(0, length.value)))
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
stringFunction('startsWith', { min: 1, max: 1 }, (value, [prefix]) =>
|
|
103
|
+
prefix === undefined ? [] : wrapBoolean(value.startsWith(prefix))
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
stringFunction('endsWith', { min: 1, max: 1 }, (value, [suffix]) =>
|
|
107
|
+
suffix === undefined ? [] : wrapBoolean(value.endsWith(suffix))
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
stringFunction('contains', { min: 1, max: 1 }, (value, [substring]) =>
|
|
111
|
+
substring === undefined ? [] : wrapBoolean(value.includes(substring))
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
stringFunction('upper', { min: 0, max: 0 }, value => str(value.toUpperCase()))
|
|
115
|
+
|
|
116
|
+
stringFunction('lower', { min: 0, max: 0 }, value => str(value.toLowerCase()))
|
|
117
|
+
|
|
118
|
+
stringFunction('replace', { min: 2, max: 2 }, (value, [pattern, substitution]) => {
|
|
119
|
+
if (pattern === undefined || substitution === undefined) {
|
|
120
|
+
return []
|
|
121
|
+
}
|
|
122
|
+
if (pattern === '') {
|
|
123
|
+
// Spec: an empty pattern surrounds every character with the substitution.
|
|
124
|
+
return str(substitution + value.split('').join(substitution) + substitution)
|
|
125
|
+
}
|
|
126
|
+
return str(value.split(pattern).join(substitution))
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
function compileRegex(name: string, pattern: string, flags: string): RegExp {
|
|
130
|
+
try {
|
|
131
|
+
// Unicode mode enables \p{...} property escapes and full code-point matching.
|
|
132
|
+
return new RegExp(pattern, `${flags}u`)
|
|
133
|
+
} catch {
|
|
134
|
+
// Patterns with annex-B quirks (e.g. unescaped braces) only compile without it.
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
return new RegExp(pattern, flags)
|
|
138
|
+
} catch {
|
|
139
|
+
throw new FhirPathTypeError(`${name}() received an invalid regular expression`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Compile with the host's regex engine (EvaluateOptions.regex) when supplied —
|
|
145
|
+
* the ReDoS protection for untrusted expressions — and the built-in RegExp
|
|
146
|
+
* otherwise. A custom engine's compile errors become the same
|
|
147
|
+
* invalid-expression type error the built-in path raises.
|
|
148
|
+
*/
|
|
149
|
+
function compilePattern(
|
|
150
|
+
name: string,
|
|
151
|
+
context: EvaluationContext,
|
|
152
|
+
pattern: string,
|
|
153
|
+
flags: string
|
|
154
|
+
): { test(subject: string): boolean; replace(subject: string, substitution: string): string } {
|
|
155
|
+
if (context.regex !== undefined) {
|
|
156
|
+
try {
|
|
157
|
+
return context.regex.compile(pattern, flags)
|
|
158
|
+
} catch {
|
|
159
|
+
throw new FhirPathTypeError(`${name}() received an invalid regular expression`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const compiled = compileRegex(name, pattern, flags)
|
|
163
|
+
return {
|
|
164
|
+
test: subject => compiled.test(subject),
|
|
165
|
+
replace: (subject, substitution) => subject.replace(compiled, substitution),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
stringFunction('matches', { min: 1, max: 1 }, (value, [pattern], context) =>
|
|
170
|
+
// Single-line mode: `.` matches line terminators (spec §5.6.9).
|
|
171
|
+
pattern === undefined ? [] : wrapBoolean(compilePattern('matches', context, pattern, 's').test(value))
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
stringFunction('matchesFull', { min: 1, max: 1 }, (value, [pattern], context) =>
|
|
175
|
+
pattern === undefined ? [] : wrapBoolean(compilePattern('matchesFull', context, `^(?:${pattern})$`, 's').test(value))
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
stringFunction('replaceMatches', { min: 2, max: 2 }, (value, [pattern, substitution], context) => {
|
|
179
|
+
if (pattern === undefined || substitution === undefined) {
|
|
180
|
+
return []
|
|
181
|
+
}
|
|
182
|
+
// An empty regex matches nothing meaningful; the input passes through unchanged.
|
|
183
|
+
if (pattern === '') {
|
|
184
|
+
return str(value)
|
|
185
|
+
}
|
|
186
|
+
return str(compilePattern('replaceMatches', context, pattern, 'gs').replace(value, substitution))
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
stringFunction('length', { min: 0, max: 0 }, value => int(value.length))
|
|
190
|
+
|
|
191
|
+
stringFunction('toChars', { min: 0, max: 0 }, value => value.split('').map(ch => ({ type: SYSTEM_STRING, value: ch })))
|
|
192
|
+
|
|
193
|
+
stringFunction('trim', { min: 0, max: 0 }, value => str(value.trim()))
|
|
194
|
+
|
|
195
|
+
stringFunction('split', { min: 1, max: 1 }, (value, [separator]) =>
|
|
196
|
+
separator === undefined ? [] : value.split(separator).map(part => ({ type: SYSTEM_STRING, value: part }))
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
// join() works on a collection of strings, not a singleton.
|
|
200
|
+
registerFunction('join', {
|
|
201
|
+
minArity: 0,
|
|
202
|
+
maxArity: 1,
|
|
203
|
+
evaluate: (context, input, args, evaluateNode) => {
|
|
204
|
+
const separator = args.length === 1 ? stringArgument('join', context, input, argAt(args, 0), evaluateNode) : ''
|
|
205
|
+
const parts: string[] = []
|
|
206
|
+
for (const item of input) {
|
|
207
|
+
if (systemTypeOf(item) !== SYSTEM_STRING) {
|
|
208
|
+
throw new FhirPathTypeError(`join() expects a collection of strings, found ${item.type}`)
|
|
209
|
+
}
|
|
210
|
+
if (item.value !== undefined) {
|
|
211
|
+
parts.push(item.value as string)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return str(parts.join(separator ?? ''))
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
219
|
+
|
|
220
|
+
function encodeBase64(value: string): string {
|
|
221
|
+
const bytes = new TextEncoder().encode(value)
|
|
222
|
+
let result = ''
|
|
223
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
224
|
+
const chunk = [bytes[i], bytes[i + 1], bytes[i + 2]]
|
|
225
|
+
const combined = ((chunk[0] as number) << 16) | (((chunk[1] ?? 0) as number) << 8) | ((chunk[2] ?? 0) as number)
|
|
226
|
+
result += BASE64_ALPHABET[(combined >> 18) & 63] as string
|
|
227
|
+
result += BASE64_ALPHABET[(combined >> 12) & 63] as string
|
|
228
|
+
result += chunk[1] === undefined ? '=' : (BASE64_ALPHABET[(combined >> 6) & 63] as string)
|
|
229
|
+
result += chunk[2] === undefined ? '=' : (BASE64_ALPHABET[combined & 63] as string)
|
|
230
|
+
}
|
|
231
|
+
return result
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function decodeBase64(name: string, value: string): string {
|
|
235
|
+
const normalized = value.replace(/=+$/, '')
|
|
236
|
+
const bytes: number[] = []
|
|
237
|
+
let buffer = 0
|
|
238
|
+
let bits = 0
|
|
239
|
+
for (const ch of normalized) {
|
|
240
|
+
const index = BASE64_ALPHABET.indexOf(ch)
|
|
241
|
+
if (index === -1) {
|
|
242
|
+
throw new FhirPathTypeError(`${name}() received invalid base64 input`)
|
|
243
|
+
}
|
|
244
|
+
buffer = (buffer << 6) | index
|
|
245
|
+
bits += 6
|
|
246
|
+
if (bits >= 8) {
|
|
247
|
+
bits -= 8
|
|
248
|
+
bytes.push((buffer >> bits) & 0xff)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return new TextDecoder().decode(new Uint8Array(bytes))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function encodeHex(value: string): string {
|
|
255
|
+
const bytes = new TextEncoder().encode(value)
|
|
256
|
+
let result = ''
|
|
257
|
+
for (const byte of bytes) {
|
|
258
|
+
result += byte.toString(16).padStart(2, '0')
|
|
259
|
+
}
|
|
260
|
+
return result
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function decodeHex(name: string, value: string): string {
|
|
264
|
+
if (value.length % 2 !== 0 || /[^0-9a-fA-F]/.test(value)) {
|
|
265
|
+
throw new FhirPathTypeError(`${name}() received invalid hex input`)
|
|
266
|
+
}
|
|
267
|
+
const bytes = new Uint8Array(value.length / 2)
|
|
268
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
269
|
+
bytes[i] = Number.parseInt(value.slice(i * 2, i * 2 + 2), 16)
|
|
270
|
+
}
|
|
271
|
+
return new TextDecoder().decode(bytes)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
stringFunction('encode', { min: 1, max: 1 }, (value, [format]) => {
|
|
275
|
+
switch (format) {
|
|
276
|
+
case undefined:
|
|
277
|
+
return []
|
|
278
|
+
case 'base64':
|
|
279
|
+
return str(encodeBase64(value))
|
|
280
|
+
case 'urlbase64':
|
|
281
|
+
return str(encodeBase64(value).replace(/\+/g, '-').replace(/\//g, '_'))
|
|
282
|
+
case 'hex':
|
|
283
|
+
return str(encodeHex(value))
|
|
284
|
+
default:
|
|
285
|
+
throw new FhirPathTypeError(`encode() does not support the format '${format}'`)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
stringFunction('decode', { min: 1, max: 1 }, (value, [format]) => {
|
|
290
|
+
switch (format) {
|
|
291
|
+
case undefined:
|
|
292
|
+
return []
|
|
293
|
+
case 'base64':
|
|
294
|
+
return str(decodeBase64('decode', value))
|
|
295
|
+
case 'urlbase64':
|
|
296
|
+
return str(decodeBase64('decode', value.replace(/-/g, '+').replace(/_/g, '/')))
|
|
297
|
+
case 'hex':
|
|
298
|
+
return str(decodeHex('decode', value))
|
|
299
|
+
default:
|
|
300
|
+
throw new FhirPathTypeError(`decode() does not support the format '${format}'`)
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const HTML_ESCAPES: Readonly<Record<string, string>> = {
|
|
305
|
+
'&': '&',
|
|
306
|
+
'<': '<',
|
|
307
|
+
'>': '>',
|
|
308
|
+
'"': '"',
|
|
309
|
+
"'": ''',
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function escapeJson(value: string): string {
|
|
313
|
+
let result = ''
|
|
314
|
+
for (const ch of value) {
|
|
315
|
+
if (ch === '"') {
|
|
316
|
+
result += '\\"'
|
|
317
|
+
} else if (ch === '\\') {
|
|
318
|
+
result += '\\\\'
|
|
319
|
+
} else if (ch === '\n') {
|
|
320
|
+
result += '\\n'
|
|
321
|
+
} else if (ch === '\r') {
|
|
322
|
+
result += '\\r'
|
|
323
|
+
} else if (ch === '\t') {
|
|
324
|
+
result += '\\t'
|
|
325
|
+
} else if (ch < ' ') {
|
|
326
|
+
result += `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`
|
|
327
|
+
} else {
|
|
328
|
+
result += ch
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return result
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Backslash sequences decode; everything else (including quote characters) is literal content. */
|
|
335
|
+
function unescapeJson(value: string): string {
|
|
336
|
+
let result = ''
|
|
337
|
+
for (let i = 0; i < value.length; i++) {
|
|
338
|
+
const ch = value[i] as string
|
|
339
|
+
if (ch !== '\\') {
|
|
340
|
+
result += ch
|
|
341
|
+
continue
|
|
342
|
+
}
|
|
343
|
+
const next = value[i + 1]
|
|
344
|
+
i += 1
|
|
345
|
+
switch (next) {
|
|
346
|
+
case '"':
|
|
347
|
+
case '\\':
|
|
348
|
+
case '/':
|
|
349
|
+
result += next
|
|
350
|
+
break
|
|
351
|
+
case 'b':
|
|
352
|
+
result += '\b'
|
|
353
|
+
break
|
|
354
|
+
case 'f':
|
|
355
|
+
result += '\f'
|
|
356
|
+
break
|
|
357
|
+
case 'n':
|
|
358
|
+
result += '\n'
|
|
359
|
+
break
|
|
360
|
+
case 'r':
|
|
361
|
+
result += '\r'
|
|
362
|
+
break
|
|
363
|
+
case 't':
|
|
364
|
+
result += '\t'
|
|
365
|
+
break
|
|
366
|
+
case 'u': {
|
|
367
|
+
const hex = value.slice(i + 1, i + 5)
|
|
368
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
369
|
+
throw new FhirPathTypeError('unescape() received invalid JSON string content')
|
|
370
|
+
}
|
|
371
|
+
result += String.fromCharCode(Number.parseInt(hex, 16))
|
|
372
|
+
i += 4
|
|
373
|
+
break
|
|
374
|
+
}
|
|
375
|
+
default:
|
|
376
|
+
throw new FhirPathTypeError('unescape() received invalid JSON string content')
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return result
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
stringFunction('escape', { min: 1, max: 1 }, (value, [target]) => {
|
|
383
|
+
switch (target) {
|
|
384
|
+
case undefined:
|
|
385
|
+
return []
|
|
386
|
+
case 'html':
|
|
387
|
+
return str(value.replace(/[&<>"']/g, ch => HTML_ESCAPES[ch] as string))
|
|
388
|
+
case 'json':
|
|
389
|
+
return str(escapeJson(value))
|
|
390
|
+
default:
|
|
391
|
+
throw new FhirPathTypeError(`escape() does not support the target '${target}'`)
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
stringFunction('unescape', { min: 1, max: 1 }, (value, [target]) => {
|
|
396
|
+
switch (target) {
|
|
397
|
+
case undefined:
|
|
398
|
+
return []
|
|
399
|
+
case 'html':
|
|
400
|
+
return str(
|
|
401
|
+
value
|
|
402
|
+
.replace(/</g, '<')
|
|
403
|
+
.replace(/>/g, '>')
|
|
404
|
+
.replace(/"/g, '"')
|
|
405
|
+
.replace(/'/g, "'")
|
|
406
|
+
.replace(/&/g, '&')
|
|
407
|
+
)
|
|
408
|
+
case 'json':
|
|
409
|
+
return str(unescapeJson(value))
|
|
410
|
+
default:
|
|
411
|
+
throw new FhirPathTypeError(`unescape() does not support the target '${target}'`)
|
|
412
|
+
}
|
|
413
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { EvaluationContext } from '../engine/context.ts'
|
|
2
|
+
import { pairEquals } from '../engine/operators/equality.ts'
|
|
3
|
+
import { FhirPathRuntimeError, FhirPathTypeError } from '../errors.ts'
|
|
4
|
+
import type { AstNode } from '../parser/ast.ts'
|
|
5
|
+
import { singleton } from '../values/collection.ts'
|
|
6
|
+
import { SYSTEM_INTEGER, type TypedValue } from '../values/typed-value.ts'
|
|
7
|
+
import { distinctItems } from './existence.ts'
|
|
8
|
+
import type { NodeEvaluator } from './iteration.ts'
|
|
9
|
+
import { argAt, registerFunction } from './registry.ts'
|
|
10
|
+
|
|
11
|
+
registerFunction('single', {
|
|
12
|
+
minArity: 0,
|
|
13
|
+
maxArity: 0,
|
|
14
|
+
evaluate: (_context, input) => {
|
|
15
|
+
if (input.length > 1) {
|
|
16
|
+
throw new FhirPathRuntimeError(`single() expects at most one item, found ${input.length}`)
|
|
17
|
+
}
|
|
18
|
+
return input
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
registerFunction('first', {
|
|
23
|
+
minArity: 0,
|
|
24
|
+
maxArity: 0,
|
|
25
|
+
evaluate: (_context, input) => input.slice(0, 1),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
registerFunction('last', {
|
|
29
|
+
minArity: 0,
|
|
30
|
+
maxArity: 0,
|
|
31
|
+
evaluate: (_context, input) => input.slice(-1),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
registerFunction('tail', {
|
|
35
|
+
minArity: 0,
|
|
36
|
+
maxArity: 0,
|
|
37
|
+
evaluate: (_context, input) => input.slice(1),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
function integerArgument(
|
|
41
|
+
name: string,
|
|
42
|
+
context: EvaluationContext,
|
|
43
|
+
input: TypedValue[],
|
|
44
|
+
node: AstNode,
|
|
45
|
+
evaluateNode: NodeEvaluator
|
|
46
|
+
): number {
|
|
47
|
+
const value = singleton(evaluateNode(node, context, input), SYSTEM_INTEGER)
|
|
48
|
+
if (value === undefined || typeof value.value !== 'number') {
|
|
49
|
+
throw new FhirPathTypeError(`${name}() expects an integer argument`)
|
|
50
|
+
}
|
|
51
|
+
return value.value
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
registerFunction('skip', {
|
|
55
|
+
minArity: 1,
|
|
56
|
+
maxArity: 1,
|
|
57
|
+
evaluate: (context, input, args, evaluateNode) => {
|
|
58
|
+
const count = integerArgument('skip', context, input, argAt(args, 0), evaluateNode)
|
|
59
|
+
return count <= 0 ? input : input.slice(count)
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
registerFunction('take', {
|
|
64
|
+
minArity: 1,
|
|
65
|
+
maxArity: 1,
|
|
66
|
+
evaluate: (context, input, args, evaluateNode) => {
|
|
67
|
+
const count = integerArgument('take', context, input, argAt(args, 0), evaluateNode)
|
|
68
|
+
return count <= 0 ? [] : input.slice(0, count)
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
registerFunction('intersect', {
|
|
73
|
+
minArity: 1,
|
|
74
|
+
maxArity: 1,
|
|
75
|
+
evaluate: (context, input, args, evaluateNode) => {
|
|
76
|
+
const other = evaluateNode(argAt(args, 0), context, input)
|
|
77
|
+
return distinctItems(input).filter(item => other.some(candidate => pairEquals(item, candidate) === true))
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
registerFunction('exclude', {
|
|
82
|
+
minArity: 1,
|
|
83
|
+
maxArity: 1,
|
|
84
|
+
evaluate: (context, input, args, evaluateNode) => {
|
|
85
|
+
const other = evaluateNode(argAt(args, 0), context, input)
|
|
86
|
+
// Keeps duplicates and order, unlike intersect().
|
|
87
|
+
return input.filter(item => !other.some(candidate => pairEquals(item, candidate) === true))
|
|
88
|
+
},
|
|
89
|
+
})
|