├── .github ├── CODEOWNERS └── workflows │ ├── pr-check.yaml │ └── test.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── fhirpath ├── .gitignore ├── compopts │ └── compopts.go ├── doc.go ├── evalopts │ └── evalopts.go ├── fhirpath.go ├── fhirpath_expression_test.go ├── fhirpath_test.go ├── fhirpathtest │ ├── fhirpathtest.go │ ├── fhirpathtest_example_test.go │ └── fhirpathtest_test.go ├── internal │ ├── compile │ │ ├── compile.go │ │ └── doc.go │ ├── expr │ │ ├── arithmetic.go │ │ ├── booleans.go │ │ ├── context.go │ │ ├── doc.go │ │ ├── expressions.go │ │ ├── expressions_test.go │ │ ├── exprtest │ │ │ ├── doc.go │ │ │ └── doubles.go │ │ └── operators.go │ ├── funcs │ │ ├── doc.go │ │ ├── function.go │ │ ├── function_table.go │ │ ├── function_table_test.go │ │ ├── function_test.go │ │ ├── impl │ │ │ ├── conversion.go │ │ │ ├── conversion_test.go │ │ │ ├── errors.go │ │ │ ├── existence.go │ │ │ ├── existence_test.go │ │ │ ├── filtering.go │ │ │ ├── filtering_test.go │ │ │ ├── math.go │ │ │ ├── math_test.go │ │ │ ├── navigation.go │ │ │ ├── navigation_test.go │ │ │ ├── not.go │ │ │ ├── not_test.go │ │ │ ├── projection.go │ │ │ ├── projection_test.go │ │ │ ├── r4.go │ │ │ ├── r4_test.go │ │ │ ├── strings.go │ │ │ ├── strings_test.go │ │ │ ├── subsetting.go │ │ │ ├── subsetting_test.go │ │ │ ├── utility.go │ │ │ └── utility_test.go │ │ └── table.go │ ├── grammar │ │ ├── fhirpath.g4 │ │ ├── fhirpath_base_visitor.go │ │ ├── fhirpath_lexer.go │ │ ├── fhirpath_parser.go │ │ ├── fhirpath_visitor.go │ │ ├── generate.go │ │ └── generate.sh │ ├── opts │ │ └── opts.go │ ├── parser │ │ ├── doc.go │ │ ├── error_handling.go │ │ ├── transforms.go │ │ └── visitor.go │ └── reflection │ │ ├── consts.go │ │ ├── doc.go │ │ ├── elements.go │ │ ├── type_specifier.go │ │ └── type_specifier_test.go ├── options.go ├── patch │ ├── doc.go │ ├── patch.go │ └── patch_test.go └── system │ ├── cmp.go │ ├── collection.go │ ├── collection_test.go │ ├── consts.go │ ├── date.go │ ├── date_test.go │ ├── date_time.go │ ├── date_time_test.go │ ├── doc.go │ ├── layouts.go │ ├── primitives.go │ ├── primitives_test.go │ ├── quantity.go │ ├── quantity_test.go │ ├── time.go │ ├── time_test.go │ ├── types.go │ └── types_test.go ├── go.mod ├── go.sum ├── gotchas.md └── internal ├── bundle ├── bundle.go ├── bundle_entry.go ├── bundle_entry_example_test.go ├── bundle_entry_test.go ├── bundle_option.go └── bundle_test.go ├── bundleopt └── bundleopt.go ├── containedresource ├── contained_resource.go ├── contained_resource_example_test.go └── contained_resource_test.go ├── element ├── canonical │ ├── canonical.go │ └── canonical_test.go ├── element.go ├── extension │ ├── extension.go │ ├── extension_example_test.go │ └── extension_test.go ├── extract.go ├── extract_test.go ├── identifier │ ├── docs.go │ ├── identifier.go │ ├── identifier_example_test.go │ ├── identifier_test.go │ └── opts.go ├── meta │ ├── meta.go │ ├── meta_example_test.go │ └── meta_test.go └── reference │ ├── identity.go │ ├── identity_test.go │ ├── literal.go │ ├── literal_test.go │ ├── reference.go │ └── reference_test.go ├── fhir ├── constraints.go ├── doc.go ├── duration.go ├── duration_test.go ├── elements_general.go ├── elements_general_test.go ├── elements_metadata.go ├── elements_primitive.go ├── elements_primitive_test.go ├── elements_special.go ├── elements_special_test.go ├── encoding.go ├── encoding_test.go ├── iface.go ├── iface_test.go ├── protofields.go ├── time.go └── time_test.go ├── fhirconv ├── doc.go ├── integer.go ├── integer_test.go ├── string.go ├── string_test.go ├── time.go └── time_test.go ├── fhirtest ├── doc.go ├── elements.go ├── identity.go ├── meta.go ├── random.go ├── reference.go ├── resource_option.go ├── resource_option_test.go ├── resources.go ├── resources_example_test.go └── resources_test.go ├── narrow ├── narrow.go ├── narrow_example_test.go └── narrow_test.go ├── protofields ├── descriptor.go ├── dummies.go ├── fields.go ├── fields_test.go ├── strcase.go ├── update.go └── update_test.go ├── resource ├── canonical_identity.go ├── canonical_identity_test.go ├── consts.go ├── contactable │ ├── contactable.go │ └── contactable_test.go ├── identity.go ├── identity_test.go ├── options.go ├── options_test.go ├── patient │ ├── patient.go │ └── patient_test.go ├── resource.go ├── resource_example_test.go ├── resource_test.go ├── type.go └── type_test.go ├── resourceopt └── resourceopt.go ├── slices ├── slices.go ├── slices_example_test.go └── slices_test.go ├── stablerand ├── doc.go └── rand.go └── units ├── doc.go └── time.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Note: For syntax, see 2 | 3 | * @verily-src/fhirpathgo-eng-reviewers 4 | -------------------------------------------------------------------------------- /.github/workflows/pr-check.yaml: -------------------------------------------------------------------------------- 1 | # Schema: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions 2 | name: Verify Contribution Guidelines 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | types: 9 | - opened 10 | - reopened 11 | - edited 12 | - synchronize 13 | 14 | env: 15 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | MESSAGE: > 17 | Thank you for your interest in this project! At this moment, we are not currently accepting community contributions in the form of PRs. 18 | If you would like to make a proposal, 19 | we will do our best to review it, implement it ourselves, and include it in the next release. 20 | If enough proposals come through, we will certainly revisit this policy to make the package as useful as possible. 21 | [Contribution Guidelines](https://github.com/verily-src/fhirpath-go/CONTRIBUTING.md). 22 | 23 | Thank you! 🙂 24 | 25 | jobs: 26 | pr-description: 27 | name: Check PR Description 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Check PR description 31 | id: check 32 | env: 33 | PR_AUTHOR: ${{ github.event.pull_request.user.login }} 34 | run: | 35 | body="${PR_AUTHOR}" 36 | body=$(echo "${body}" | sed -E -e '/OliverCardoza|bitwizeshift|VickSuresh|assefamaru|jonhayesverily|alexlaurinmath|Copybara/!d') 37 | 38 | if [ -z "$body"]; then 39 | echo "status=failure" >> "${GITHUB_OUTPUT}" 40 | else 41 | echo "status=success" >> "${GITHUB_OUTPUT}" 42 | fi 43 | 44 | # This stage is used to see whether we want to delete the comment once the 45 | # problem has been resolved. 46 | - name: Check for existing comment 47 | id: comment 48 | if: steps.check.outputs.status == 'failure' 49 | continue-on-error: true 50 | env: 51 | PR_NUM: ${{ github.event.pull_request.number }} 52 | run: | 53 | # Check if comment already exists 54 | comment_id=$(gh api \ 55 | -H "Accept: application/vnd.github+json" \ 56 | -H "X-GitHub-Api-Version: 2022-11-28" \ 57 | "repos/verily-src/verily1/issues/${{ env.PR_NUM }}/comments" \ 58 | --jq ".[] | select(.body | contains(\"${{ env.MESSAGE }}\")) | .id") 59 | 60 | echo "id=${comment_id}" >> "${GITHUB_OUTPUT}" 61 | 62 | - name: Post comment 63 | env: 64 | PR_NUM: ${{ github.event.pull_request.number }} 65 | PR_URL: ${{ github.event.pull_request.html_url }} 66 | AUTHOR: "@${{ github.event.pull_request.user.login }}" 67 | if: steps.check.outputs.status == 'failure' && steps.comment.outputs.id == '' 68 | continue-on-error: true 69 | run: | 70 | gh pr comment "${{ env.PR_URL }}" -b "${{ env.AUTHOR }} ${{ env.MESSAGE }}" 71 | 72 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build-test: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Setup Go 14 | uses: actions/setup-go@v5 15 | with: 16 | # Use the central Go version defined in go.mod to make it easier 17 | # to perform upgrades. 18 | go-version-file: go.mod 19 | - name: Vet 20 | run: go vet -v -unreachable=false ./... 21 | - name: Build 22 | run: go build -v ./... 23 | - name: Test 24 | run: go test ./... 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Reporting issues 4 | 5 | Bugs, feature requests, and development-related questions should be directed to our GitHub issue tracker. If reporting a bug, please try and provide as much context as possible such as your operating system, Go version, and anything else that might be relevant to the bug. For feature requests, please explain what you're trying to do, and how the requested feature would help you do that. 6 | 7 | ## Pull Request 8 | 9 | We don't currently accept pull requests from the public. They will be automatically closed. If you would like to make a proposal, we will do our best to review it, implement it ourselves, and include it in the next release. If enough proposals come through, we will certainly revisit this policy to make the package as useful as possible. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Verily 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FHIRPath 2 | 3 | Developed and maintained by the nice folks at [Verily](https://verily.com/). 4 | 5 | This package contains a Go implementation of the [FHIRPath][fhirpath] specification, implemented directly with 6 | the [google/fhir][google-fhir] proto definitions. 7 | 8 | This package aims to be compliant with both: 9 | 10 | - the [N1 Normative Release](http://hl7.org/fhirpath/N1/) specification, and 11 | - the [R4 specifications](http://hl7.org/fhir/R4/fhirpath.html). 12 | 13 | ## Import 14 | 15 | ```go 16 | import "github.com/verily-src/fhirpath-go/fhirpath" 17 | ``` 18 | 19 | ## Usage 20 | 21 | A FHIRPath must be compiled before running it against a resource using the `Compile` method like so: 22 | 23 | ```go 24 | expression, err := fhirpath.Compile("Patient.name.given") 25 | if err != nil { 26 | panic("error while compiling FHIRPath") 27 | } 28 | ``` 29 | 30 | The compilation result can then be run against a resource: 31 | 32 | ```go 33 | inputResources := []fhir.Resource{somePatient, someMedication} 34 | 35 | result, err := expression.Evaluate(inputResources) 36 | if err != nil { 37 | panic("error while running FHIRPath against resource") 38 | } 39 | ``` 40 | 41 | As defined in the FHIRPath specification, the output of evaluation is a **Collection**. So, the 42 | result of Evaluate is of type `[]any`. As such, the result must be unpacked and cast to the desired 43 | type for further processing. 44 | 45 | ### CompileOptions and EvaluateOptions 46 | 47 | Options are provided for optional modification of compilation and evaluation. There is currently 48 | support for: 49 | 50 | - adding custom functions during Compile time 51 | - adding custom external constant variables 52 | 53 | #### To add a custom function 54 | 55 | The constraints on the custom function are as follows: 56 | 57 | - First argument must be `system.Collection` 58 | - Arguments that follow must be either a fhir proto type or primitive system type 59 | 60 | ```go 61 | customFn := func (input system.Collection, args ...any) (system.Collection error) { 62 | fmt.Print("called custom fn") 63 | return input, nil 64 | } 65 | expression, err := fhirpath.Compile("print()", compopts.AddFunction("print", customFn)) 66 | ``` 67 | 68 | #### To add external constants 69 | 70 | The constraints on external constants are as follows: 71 | 72 | - Must be a fhir proto type, primitive system type, or `system.Collection` 73 | - If you pass in a collection, contained elements must be fhir proto or system type. 74 | 75 | ```go 76 | customVar := system.String("custom variable") 77 | result, err := expression.Evaluate([]fhir.Resource{someResource}, evalopts.EnvVariable("var", customVar)) 78 | ``` 79 | 80 | ### System Types 81 | 82 | The FHIRPath [spec](http://hl7.org/fhirpath/N1/#literals) defines the following custom System types: 83 | 84 | - Boolean 85 | - String 86 | - Integer 87 | - Decimal 88 | - Quantity 89 | - Date 90 | - Time 91 | - DateTime 92 | 93 | FHIR Protos get implicitly converted to the above types according to this 94 | [chart](http://hl7.org/fhir/R4/fhirpath.html#types), when used in some FHIRPath expressions. 95 | 96 | ### Things to be aware of 97 | 98 | FHIRPath is not the most intuitive language, and there are some quirks. See [gotchas](gotchas.md). 99 | 100 | [fhirpath]: http://hl7.org/fhirpath/ 101 | [google-fhir]: https://github.com/google/fhir 102 | -------------------------------------------------------------------------------- /fhirpath/.gitignore: -------------------------------------------------------------------------------- 1 | *antlr*.jar 2 | *.interp 3 | *.tokens 4 | -------------------------------------------------------------------------------- /fhirpath/compopts/compopts.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package compopts provides CompileOption values for FHIRPath. 3 | 4 | This package exists to isolate the options away from the core FHIRPath logic, 5 | since this will simplify discovery of compile-specific options. 6 | */ 7 | package compopts 8 | 9 | import ( 10 | "errors" 11 | 12 | "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs" 13 | "github.com/verily-src/fhirpath-go/fhirpath/internal/opts" 14 | "github.com/verily-src/fhirpath-go/fhirpath/internal/parser" 15 | ) 16 | 17 | var ErrMultipleTransforms = errors.New("multiple transforms provided") 18 | 19 | // AddFunction creates a CompileOption that will register a custom FHIRPath 20 | // function that can be called during evaluation with the given name. 21 | // 22 | // If the function already exists, then compilation will return an error. 23 | func AddFunction(name string, fn any) opts.CompileOption { 24 | return opts.Transform(func(cfg *opts.CompileConfig) error { 25 | return cfg.Table.Register(name, fn) 26 | }) 27 | } 28 | 29 | // Transform creates a CompileOption that will set a transform 30 | // to be called on each expression returned by the Visitor. 31 | // 32 | // If there is already a Transform set, then compilation will return an error. 33 | func Transform(v parser.VisitorTransform) opts.CompileOption { 34 | return opts.Transform(func(cfg *opts.CompileConfig) error { 35 | if cfg.Transform != nil { 36 | return ErrMultipleTransforms 37 | } 38 | cfg.Transform = v 39 | return nil 40 | }) 41 | } 42 | 43 | // Permissive is an option that enables deprecated behavior in FHIRPath field 44 | // navigation. This can be used as a temporary fix for FHIRpaths that have never 45 | // been valid FHIRPaths, but have worked up until this point. 46 | // 47 | // This option is marked Deprecated so that it nags users until the paths can 48 | // be resolved. 49 | // 50 | // Deprecated: Please update FHIRPaths whenever possible. 51 | func Permissive() opts.CompileOption { 52 | return opts.Transform(func(cfg *opts.CompileConfig) error { 53 | cfg.Permissive = true 54 | return nil 55 | }) 56 | } 57 | 58 | // WithExperimentalFuncs is an option that enables experimental functions not 59 | // in the N1 Normative specification. 60 | func WithExperimentalFuncs() opts.CompileOption { 61 | return opts.Transform(func(cfg *opts.CompileConfig) error { 62 | cfg.Table = funcs.AddExperimentalFuncs(cfg.Table) 63 | return nil 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /fhirpath/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package fhirpath implements the FHIRPath specification. 3 | 4 | More documentation about the FHIRPath specification can be found on HL7 5 | websites: 6 | * https://www.hl7.org/fhir/fhirpath.html 7 | * http://hl7.org/fhirpath/N1/ 8 | */ 9 | package fhirpath 10 | -------------------------------------------------------------------------------- /fhirpath/evalopts/evalopts.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package evalopts provides EvaluateOption values for FHIRPath. 3 | 4 | This package exists to isolate the options away from the core FHIRPath logic, 5 | since this will simplify discovery of evaluation-specific options. 6 | */ 7 | package evalopts 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "time" 13 | 14 | "github.com/verily-src/fhirpath-go/fhirpath/internal/opts" 15 | "github.com/verily-src/fhirpath-go/fhirpath/system" 16 | "github.com/verily-src/fhirpath-go/internal/fhir" 17 | ) 18 | 19 | var ( 20 | ErrUnsupportedType = errors.New("external constant type not supported") 21 | ErrExistingConstant = errors.New("constant already exists") 22 | ) 23 | 24 | // OverrideTime returns an EvaluateOption that can be used to override the time 25 | // that will be used in FHIRPath expressions. 26 | func OverrideTime(t time.Time) opts.EvaluateOption { 27 | return opts.Transform(func(cfg *opts.EvaluateConfig) error { 28 | cfg.Context.Now = t 29 | return nil 30 | }) 31 | } 32 | 33 | // EnvVariable returns an EvaluateOption that sets FHIRPath environment variables 34 | // (e.g. %action). 35 | // 36 | // The input must be one of: 37 | // - A FHIRPath System type, 38 | // - A FHIR Element or Resource type, or 39 | // - A FHIRPath Collection, containing the above types. 40 | // 41 | // If an EnvVariable is specified that already exists in the expression, then 42 | // evaluation will yield an ErrExistingConstant error. If an EnvVariable is 43 | // contains a type that is not one of the above valid types, then evaluation 44 | // will yield an ErrUnsupportedType error. 45 | func EnvVariable(name string, value any) opts.EvaluateOption { 46 | return opts.Transform(func(cfg *opts.EvaluateConfig) error { 47 | if err := validateType(value); err != nil { 48 | return err 49 | } 50 | if _, ok := cfg.Context.ExternalConstants[name]; !ok { 51 | cfg.Context.ExternalConstants[name] = value 52 | return nil 53 | } 54 | return fmt.Errorf("%w: %s", ErrExistingConstant, name) 55 | }) 56 | } 57 | 58 | // validateType validates that the input type is a supported 59 | // fhir proto or System type. If a system.Collection is passed in, 60 | // recursively checks each element. 61 | func validateType(input any) error { 62 | var err error 63 | switch v := input.(type) { 64 | case fhir.Base, system.Any: 65 | break 66 | case system.Collection: 67 | for _, elem := range v { 68 | err = errors.Join(err, validateType(elem)) 69 | } 70 | default: 71 | err = fmt.Errorf("%w: %T", ErrUnsupportedType, input) 72 | } 73 | return err 74 | } 75 | -------------------------------------------------------------------------------- /fhirpath/fhirpath.go: -------------------------------------------------------------------------------- 1 | package fhirpath 2 | 3 | import ( 4 | "errors" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/verily-src/fhirpath-go/fhirpath/evalopts" 8 | "github.com/verily-src/fhirpath-go/fhirpath/internal/compile" 9 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 10 | "github.com/verily-src/fhirpath-go/fhirpath/internal/opts" 11 | "github.com/verily-src/fhirpath-go/fhirpath/internal/parser" 12 | "github.com/verily-src/fhirpath-go/fhirpath/system" 13 | "github.com/verily-src/fhirpath-go/internal/fhir" 14 | "github.com/verily-src/fhirpath-go/internal/slices" 15 | ) 16 | 17 | var ( 18 | ErrInvalidField = expr.ErrInvalidField 19 | ErrUnsupportedType = evalopts.ErrUnsupportedType 20 | ErrExistingConstant = evalopts.ErrExistingConstant 21 | ) 22 | 23 | // Resource is a FHIR resource. This is an alias for the 24 | // fhir.Resource type, which is the base type for all FHIR resources. 25 | type Resource = fhir.Resource 26 | 27 | // Expression is the FHIRPath expression that will be compiled from a FHIRPath string 28 | type Expression struct { 29 | expression expr.Expression 30 | path string 31 | } 32 | 33 | // Compile parses and compiles the FHIRPath expression down to a single 34 | // Expression object. 35 | // 36 | // If there are any syntax or semantic errors, this will return an 37 | // error indicating the compilation failure reason. 38 | func Compile(expr string, options ...CompileOption) (*Expression, error) { 39 | config, err := compile.PopulateConfig(options...) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | tree, err := compile.Tree(expr) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | visitor := &parser.FHIRPathVisitor{ 50 | Functions: config.Table, 51 | Permissive: config.Permissive, 52 | } 53 | vr, ok := visitor.Visit(tree).(*parser.VisitResult) 54 | if !ok { 55 | return nil, errors.New("input expression currently unsupported") 56 | } 57 | 58 | if vr.Error != nil { 59 | return nil, vr.Error 60 | } 61 | return &Expression{ 62 | expression: vr.Result, 63 | path: expr, 64 | }, nil 65 | } 66 | 67 | // String returns the string representation of this FHIRPath expression. 68 | // This is just the input that initially produced the FHIRPath value. 69 | func (e *Expression) String() string { 70 | return e.path 71 | } 72 | 73 | // MustCompile compiles the FHIRpath expression input, and returns the 74 | // compiled expression. If any compilation error occurs, this function 75 | // will panic. 76 | func MustCompile(expr string, opts ...CompileOption) *Expression { 77 | result, err := Compile(expr, opts...) 78 | if err != nil { 79 | panic(err) 80 | } 81 | return result 82 | } 83 | 84 | // Evaluate the expression, returning either a collection of elements, or error 85 | func (e *Expression) Evaluate(input []Resource, options ...EvaluateOption) (system.Collection, error) { 86 | config := &opts.EvaluateConfig{ 87 | Context: expr.InitializeContext(slices.MustConvert[any](input)), 88 | } 89 | config, err := opts.ApplyOptions(config, options...) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | collection := slices.MustConvert[any](input) 95 | return e.expression.Evaluate(config.Context, collection) 96 | } 97 | 98 | // EvaluateAsString evaluates the expression, returning a string or error 99 | func (e *Expression) EvaluateAsString(input []Resource, options ...EvaluateOption) (string, error) { 100 | got, err := e.Evaluate(input, options...) 101 | if err != nil { 102 | return "", err 103 | } 104 | return got.ToString() 105 | } 106 | 107 | // EvaluateAsBool evaluates the expression, returning either a boolean or error 108 | func (e *Expression) EvaluateAsBool(input []Resource, options ...EvaluateOption) (bool, error) { 109 | got, err := e.Evaluate(input, options...) 110 | if err != nil { 111 | return false, err 112 | } 113 | return got.ToBool() 114 | } 115 | 116 | // EvaluateAsInt32 evaluates the expression, returning either an int32 or error 117 | func (e *Expression) EvaluateAsInt32(input []Resource, options ...EvaluateOption) (int32, error) { 118 | got, err := e.Evaluate(input, options...) 119 | if err != nil { 120 | return 0, err 121 | } 122 | return got.ToInt32() 123 | } 124 | 125 | // EvaluateAsCanonical evaluates the expression, returning a FHIR canonical or error 126 | func (e *Expression) EvaluateAsCanonical(input []Resource, options ...EvaluateOption) (*dtpb.Canonical, error) { 127 | got, err := e.Evaluate(input, options...) 128 | if err != nil { 129 | return nil, err 130 | } 131 | return got.ToCanonical() 132 | } 133 | -------------------------------------------------------------------------------- /fhirpath/fhirpathtest/fhirpathtest.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package fhirpathtest provides an easy way to generate test-doubles within 3 | FHIRPath. 4 | */ 5 | package fhirpathtest 6 | 7 | import ( 8 | "github.com/verily-src/fhirpath-go/fhirpath" 9 | "github.com/verily-src/fhirpath-go/fhirpath/system" 10 | ) 11 | 12 | // Return creates a FHIRPath expression that will always return the given 13 | // values. 14 | func Return(args ...any) *fhirpath.Expression { 15 | return ReturnCollection(system.Collection(args)) 16 | } 17 | 18 | // ReturnCollection creates a FHIRPath expression that will always return the 19 | // given input collection. 20 | func ReturnCollection(collection system.Collection) *fhirpath.Expression { 21 | return fhirpath.MustCompile("return()", 22 | fhirpath.WithFunction("return", func(system.Collection) (system.Collection, error) { 23 | return collection, nil 24 | }), 25 | ) 26 | } 27 | 28 | // Error creates a FHIRPath expression that will always return the specified error. 29 | func Error(err error) *fhirpath.Expression { 30 | return fhirpath.MustCompile("return()", 31 | fhirpath.WithFunction("return", func(system.Collection) (system.Collection, error) { 32 | return nil, err 33 | }), 34 | ) 35 | } 36 | 37 | var ( 38 | // Empty is a FHIRPath expression that returns an empty collection when 39 | // evaluated. 40 | Empty = Return(system.Collection{}) 41 | 42 | // True is a FHIRPath expression that returns a collection containing a single 43 | // system boolean of 'true'. This is useful for testing expected boolean 44 | // logic in paths. 45 | True = Return(system.Collection{system.Boolean(true)}) 46 | 47 | // False is a FHIRPath expression that returns a collection containing a single 48 | // system boolean of 'false'. This is useful for testing expected boolean 49 | // logic in paths. 50 | False = Return(system.Collection{system.Boolean(false)}) 51 | ) 52 | -------------------------------------------------------------------------------- /fhirpath/fhirpathtest/fhirpathtest_example_test.go: -------------------------------------------------------------------------------- 1 | package fhirpathtest_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/verily-src/fhirpath-go/fhirpath" 9 | "github.com/verily-src/fhirpath-go/fhirpath/fhirpathtest" 10 | "github.com/verily-src/fhirpath-go/fhirpath/system" 11 | 12 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 13 | pb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" 14 | ) 15 | 16 | func ExampleError() { 17 | want := errors.New("example error") 18 | expr := fhirpathtest.Error(want) 19 | 20 | _, err := expr.Evaluate([]fhirpath.Resource{}) 21 | if errors.Is(err, want) { 22 | fmt.Printf("err = '%v'", want) 23 | } 24 | 25 | // Output: err = 'example error' 26 | } 27 | 28 | func ExampleReturn() { 29 | want := system.Boolean(true) 30 | expr := fhirpathtest.Return(want) 31 | 32 | got, err := expr.Evaluate([]fhirpath.Resource{}) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | fmt.Printf("got = %v", bool(got[0].(system.Boolean))) 38 | // Output: got = true 39 | } 40 | func ExampleReturnCollection() { 41 | want := system.Collection{system.Boolean(true)} 42 | expr := fhirpathtest.ReturnCollection(want) 43 | 44 | got, err := expr.Evaluate([]fhirpath.Resource{}) 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | fmt.Printf("got = %v", bool(got[0].(system.Boolean))) 50 | // Output: got = true 51 | } 52 | 53 | // TestFHIRPathResource is based on issue can't call Evaluate() because fhir.Resource is internal? #18 54 | // https://github.com/verily-src/fhirpath-go/issues/18 55 | // 56 | // Testing that by using only the fhirpath package, we can still evaluate 57 | // a FHIRPath expression that returns a resource. 58 | func TestFHIRPathResource(t *testing.T) { 59 | want := system.Collection{ 60 | &dtpb.String{Value: "John"}, 61 | } 62 | // Create a FHIR Patient resource 63 | patient := &pb.Patient{ 64 | Name: []*dtpb.HumanName{ 65 | { 66 | Given: []*dtpb.String{ 67 | {Value: "John"}, 68 | }, 69 | Family: &dtpb.String{Value: "Doe"}, 70 | }, 71 | }, 72 | } 73 | 74 | // Compile the FHIRPath expression 75 | expr := fhirpath.MustCompile("name.given") 76 | 77 | // Wrap the Patient resource in a FHIRPath Resource 78 | resource := []fhirpath.Resource{patient} 79 | 80 | // Evaluate the expression against the Patient resource 81 | got, err := expr.Evaluate(resource) 82 | if err != nil { 83 | t.Errorf("Error evaluating FHIRPath expression: %v", err) 84 | } 85 | 86 | // Check if the results match the expected output 87 | if len(got) != 1 { 88 | t.Errorf("Expected 1 result, got %d", len(got)) 89 | } 90 | gotValue := got[0].(*dtpb.String).GetValue() 91 | wantValue := want[0].(*dtpb.String).GetValue() 92 | if gotValue != wantValue { 93 | t.Errorf("Expected %s, got %s", wantValue, gotValue) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /fhirpath/fhirpathtest/fhirpathtest_test.go: -------------------------------------------------------------------------------- 1 | package fhirpathtest_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/verily-src/fhirpath-go/fhirpath" 9 | "github.com/verily-src/fhirpath-go/fhirpath/fhirpathtest" 10 | "github.com/verily-src/fhirpath-go/fhirpath/system" 11 | "github.com/verily-src/fhirpath-go/internal/fhirtest" 12 | "github.com/verily-src/fhirpath-go/internal/resource" 13 | "google.golang.org/protobuf/testing/protocmp" 14 | ) 15 | 16 | func TestError_Evaluates_ReturnsErr(t *testing.T) { 17 | wantErr := errors.New("test error") 18 | expr := fhirpathtest.Error(wantErr) 19 | 20 | _, err := expr.Evaluate([]fhirpath.Resource{}) 21 | 22 | if got, want := err, wantErr; !errors.Is(got, want) { 23 | t.Errorf("Error: want err %v, got %v", want, got) 24 | } 25 | } 26 | 27 | func TestReturn_Evaluates_ReturnsCollectionOfEntries(t *testing.T) { 28 | res := fhirtest.NewResource(t, resource.Patient) 29 | want := system.Collection{res} 30 | expr := fhirpathtest.Return(res) 31 | 32 | got, err := expr.Evaluate([]fhirpath.Resource{}) 33 | if err != nil { 34 | t.Fatalf("Return: unexpected err: %v", err) 35 | } 36 | 37 | if len(got) != len(want) { 38 | t.Fatalf("Return: mismatched size; want %v, got %v", len(got), len(want)) 39 | } 40 | if !cmp.Equal(got[0], want[0], protocmp.Transform()) { 41 | t.Errorf("Return: want %v, got %v", want, got) 42 | } 43 | } 44 | 45 | func TestReturnCollection_Evaluates_ReturnsCollectionOfEntries(t *testing.T) { 46 | res := fhirtest.NewResource(t, resource.Patient) 47 | want := system.Collection{res} 48 | expr := fhirpathtest.ReturnCollection(want) 49 | 50 | got, err := expr.Evaluate([]fhirpath.Resource{}) 51 | if err != nil { 52 | t.Fatalf("Return: unexpected err: %v", err) 53 | } 54 | 55 | if len(got) != len(want) { 56 | t.Fatalf("ReturnCollection: mismatched size; want %v, got %v", len(got), len(want)) 57 | } 58 | if !cmp.Equal(got[0], want[0], protocmp.Transform()) { 59 | t.Errorf("ReturnCollection: want %v, got %v", want, got) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /fhirpath/internal/compile/compile.go: -------------------------------------------------------------------------------- 1 | package compile 2 | 3 | import ( 4 | "github.com/antlr4-go/antlr/v4" 5 | "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs" 6 | "github.com/verily-src/fhirpath-go/fhirpath/internal/grammar" 7 | "github.com/verily-src/fhirpath-go/fhirpath/internal/opts" 8 | "github.com/verily-src/fhirpath-go/fhirpath/internal/parser" 9 | ) 10 | 11 | // PopulateConfig creates a CompileConfig and prepopulates it with 12 | // a function table and any provided options. 13 | func PopulateConfig(options ...opts.CompileOption) (*opts.CompileConfig, error) { 14 | config := &opts.CompileConfig{ 15 | Table: funcs.Clone(), 16 | } 17 | config, err := opts.ApplyOptions(config, options...) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return config, err 22 | } 23 | 24 | // Tree creates an ANTLR parsing context from the provided FHIRPath string. 25 | func Tree(expr string) (grammar.IProgContext, error) { 26 | inputStream := antlr.NewInputStream(expr) 27 | errorListener := &parser.FHIRPathErrorListener{} 28 | 29 | // Lex the input stream 30 | lexer := grammar.NewfhirpathLexer(inputStream) 31 | lexer.RemoveErrorListeners() 32 | lexer.AddErrorListener(errorListener) 33 | tokens := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) 34 | 35 | // Parse the tokens 36 | p := grammar.NewfhirpathParser(tokens) 37 | p.RemoveErrorListeners() 38 | p.AddErrorListener(errorListener) 39 | tree := p.Prog() 40 | 41 | if err := errorListener.Error(); err != nil { 42 | return nil, err 43 | } 44 | 45 | return tree, nil 46 | } 47 | -------------------------------------------------------------------------------- /fhirpath/internal/compile/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package compile provides helpers for compiling 3 | FHIRPath strings into FHIRPath expressions. 4 | */ 5 | package compile 6 | -------------------------------------------------------------------------------- /fhirpath/internal/expr/booleans.go: -------------------------------------------------------------------------------- 1 | package expr 2 | 3 | import "github.com/verily-src/fhirpath-go/fhirpath/system" 4 | 5 | func evaluateAnd(left []system.Boolean, right []system.Boolean) system.Collection { 6 | if len(left) > 0 && len(right) > 0 { 7 | result := system.Boolean(left[0] && right[0]) 8 | return system.Collection{result} 9 | } 10 | // returns false if either boolean is false, regardless of whether or not the other is empty. 11 | if (len(left) == 1 && !left[0]) || (len(right) == 1 && !right[0]) { 12 | return system.Collection{system.Boolean(false)} 13 | } 14 | return system.Collection{} 15 | } 16 | 17 | func evaluateOr(left []system.Boolean, right []system.Boolean) system.Collection { 18 | if len(left) > 0 && len(right) > 0 { 19 | result := system.Boolean(left[0] || right[0]) 20 | return system.Collection{result} 21 | } 22 | // returns false if either boolean is true, regardless of whether or not the other is empty. 23 | if (len(left) == 1 && left[0]) || (len(right) == 1 && right[0]) { 24 | return system.Collection{system.Boolean(true)} 25 | } 26 | return system.Collection{} 27 | } 28 | 29 | func evaluateXor(left []system.Boolean, right []system.Boolean) system.Collection { 30 | if len(left) > 0 && len(right) > 0 { 31 | result := system.Boolean(left[0] != right[0]) 32 | return system.Collection{result} 33 | } 34 | return system.Collection{} 35 | } 36 | 37 | func evaluateImplies(left []system.Boolean, right []system.Boolean) system.Collection { 38 | if len(left) > 0 && len(right) > 0 { 39 | result := system.Boolean(!left[0] || right[0]) 40 | return system.Collection{result} 41 | } 42 | // returns true if left is false, or if right is true, regardless of whether or not the other is empty. 43 | if (len(left) > 0 && !left[0]) || (len(right) > 0 && right[0]) { 44 | return system.Collection{system.Boolean(true)} 45 | } 46 | return system.Collection{} 47 | } 48 | -------------------------------------------------------------------------------- /fhirpath/internal/expr/context.go: -------------------------------------------------------------------------------- 1 | package expr 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/verily-src/fhirpath-go/fhirpath/system" 7 | ) 8 | 9 | // Context holds the global time and external constant 10 | // variable map, to enable deterministic evaluation. 11 | type Context struct { 12 | Now time.Time 13 | ExternalConstants map[string]any 14 | 15 | // LastResult is required for implementing most FHIRPatch operations, since 16 | // a reference to the node before the one being (inserted, replaced, moved) is 17 | // necessary in order to alter the containing object. 18 | LastResult system.Collection 19 | 20 | // BeforeLastResult is necessary for implementing FHIRPatch delete due to an 21 | // edge-case, where deleting a specific element from a list requires a pointer 22 | // to the container that holds the list. In a path like `Patient.name.given[0]`, 23 | // the 'LastResult' will be the unwrapped list from 'given', but we need the 24 | // 'name' element that contains the 'given' list in order to alter the list. 25 | BeforeLastResult system.Collection 26 | } 27 | 28 | // Clone copies this Context object to produce a new instance. 29 | func (c *Context) Clone() *Context { 30 | return &Context{ 31 | Now: c.Now, 32 | ExternalConstants: c.ExternalConstants, 33 | LastResult: c.LastResult, 34 | } 35 | } 36 | 37 | // InitializeContext returns a base context, initialized with current time and initial 38 | // constant variables set. 39 | func InitializeContext(input system.Collection) *Context { 40 | return &Context{ 41 | Now: time.Now().Local().UTC(), 42 | ExternalConstants: map[string]any{ 43 | "context": input, 44 | "ucum": system.String("http://unitsofmeasure.org"), 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /fhirpath/internal/expr/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package expr contains all the expression types 3 | and related logic for FHIRPath expressions. 4 | */ 5 | package expr 6 | -------------------------------------------------------------------------------- /fhirpath/internal/expr/exprtest/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package exprtest provides some useful test dummies to make it 3 | easier to mock expressions to return a desired result. 4 | */ 5 | package exprtest 6 | -------------------------------------------------------------------------------- /fhirpath/internal/expr/exprtest/doubles.go: -------------------------------------------------------------------------------- 1 | package exprtest 2 | 3 | import ( 4 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 5 | "github.com/verily-src/fhirpath-go/fhirpath/system" 6 | ) 7 | 8 | // MockExpression is a test double expression that calls 9 | // the contained function when evaluated. 10 | type MockExpression struct { 11 | Eval func(*expr.Context, system.Collection) (system.Collection, error) 12 | } 13 | 14 | // Evaluate calls the contained Eval function. 15 | func (e *MockExpression) Evaluate(ctx *expr.Context, input system.Collection) (system.Collection, error) { 16 | return e.Eval(ctx, input) 17 | } 18 | 19 | // Error creates a MockExpression that returns the provided 20 | // error when evaluated. 21 | func Error(input error) *MockExpression { 22 | return &MockExpression{ 23 | func(*expr.Context, system.Collection) (system.Collection, error) { 24 | return nil, input 25 | }, 26 | } 27 | } 28 | 29 | // Return creates a MockExpression that returns the provided 30 | // inputs when evaluated. 31 | func Return(out ...any) *MockExpression { 32 | return &MockExpression{ 33 | func(*expr.Context, system.Collection) (system.Collection, error) { 34 | return out, nil 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /fhirpath/internal/expr/operators.go: -------------------------------------------------------------------------------- 1 | package expr 2 | 3 | // Operator constants. 4 | const ( 5 | Equals = "=" 6 | NotEquals = "!=" 7 | Equivalence = "~" 8 | Inequivalence = "!~" 9 | Is = "is" 10 | As = "as" 11 | And = "and" 12 | Or = "or" 13 | Xor = "xor" 14 | Implies = "implies" 15 | Lt = "<" 16 | Gt = ">" 17 | Lte = "<=" 18 | Gte = ">=" 19 | Add = "+" 20 | Sub = "-" 21 | Concat = "&" 22 | Mul = "*" 23 | Div = "/" 24 | FloorDiv = "div" 25 | Mod = "mod" 26 | ) 27 | 28 | // Operator represents a valid expression operator. 29 | type Operator string 30 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package funcs provides the implementations for all 3 | base FHIRPath functions. Provides a wrapper, and function table 4 | for compilation. 5 | */ 6 | package funcs 7 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/function.go: -------------------------------------------------------------------------------- 1 | package funcs 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 9 | "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" 10 | "github.com/verily-src/fhirpath-go/fhirpath/system" 11 | ) 12 | 13 | var ( 14 | errNotFunc = errors.New("value is not a function") 15 | errMissingArgs = errors.New("missing arguments") 16 | errInvalidParams = errors.New("invalid input parameters") 17 | errInvalidReturn = errors.New("invalid function return signature") 18 | ) 19 | 20 | var notImplemented = Function{Func: unimplemented} 21 | 22 | // FHIRPathFunc is the common abstraction for all function types 23 | // supported by FHIRPath. 24 | type FHIRPathFunc func(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) 25 | 26 | // Function contains the FHIRPathFunction, along with metadata 27 | type Function struct { 28 | Func FHIRPathFunc 29 | MinArity int 30 | MaxArity int 31 | IsTypeFunction bool 32 | } 33 | 34 | // ToFunction takes in a function with any arguments and attempts to 35 | // convert it to a functions.Function type. If the conversion is successful, 36 | // the new function will assert the argument expressions resolve to the original 37 | // argument types. 38 | func ToFunction(fn any) (Function, error) { 39 | rv := reflect.ValueOf(fn) 40 | if err := validateFunc(rv); err != nil { 41 | return Function{}, fmt.Errorf("constructing FHIRPathFunction: %w", err) 42 | } 43 | 44 | arity := rv.Type().NumIn() - 1 // 'True' arity, as the first argument is the input Collection. 45 | fhirpathFunc := func(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { 46 | if len(args) != arity { 47 | return nil, fmt.Errorf("%w: function expects %v arguments, received %v", impl.ErrWrongArity, arity, len(args)) 48 | } 49 | // ensure the arguments match the function signature. 50 | funcArgs := []reflect.Value{reflect.ValueOf(input)} 51 | for i, exp := range args { 52 | result, err := exp.Evaluate(ctx, input) 53 | if err != nil { 54 | return nil, err 55 | } 56 | if len(result) != 1 { 57 | return nil, fmt.Errorf("%w: doesn't return singleton", impl.ErrInvalidReturnType) 58 | } 59 | if expectedType, gotType := rv.Type().In(i+1), reflect.TypeOf(result[0]); !gotType.AssignableTo(expectedType) { 60 | return nil, fmt.Errorf("%w: got type '%s' when type '%s' was expected", impl.ErrInvalidReturnType, gotType.String(), expectedType.Name()) 61 | } 62 | funcArgs = append(funcArgs, reflect.ValueOf(result[0])) 63 | } 64 | output := rv.Call(funcArgs) 65 | if err, ok := output[1].Interface().(error); ok { 66 | return output[0].Interface().(system.Collection), err 67 | } 68 | return output[0].Interface().(system.Collection), nil 69 | } 70 | return Function{fhirpathFunc, arity, arity, false}, nil 71 | } 72 | 73 | // validateFunc verifies that the input reflect value represents a 74 | // valid FHIRPath function. If not, it returns an error. 75 | func validateFunc(rv reflect.Value) error { 76 | if rv.Kind() != reflect.Func { 77 | return errNotFunc 78 | } 79 | errs := []error{} 80 | if rv.Type().NumIn() < 1 { 81 | errs = append(errs, errMissingArgs) 82 | } else if rv.Type().In(0) != reflect.TypeOf(system.Collection{}) { 83 | errs = append(errs, errInvalidParams) 84 | } 85 | if rv.Type().NumOut() != 2 || rv.Type().Out(0) != reflect.TypeOf(system.Collection{}) || rv.Type().Out(1).Name() != "error" { 86 | errs = append(errs, errInvalidReturn) 87 | } 88 | return errors.Join(errs...) 89 | } 90 | 91 | // unimplemented is a no-op placeholder function that satisfies the FHIRPathFunction contract 92 | func unimplemented(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { 93 | return nil, fmt.Errorf("function not yet implemented") 94 | } 95 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/function_table.go: -------------------------------------------------------------------------------- 1 | package funcs 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // FunctionTable is the data structure used to store 8 | // valid FHIRPath functions, and maps their case-sensitive 9 | // names. 10 | type FunctionTable map[string]Function 11 | 12 | // Register attempts to add a given function to the FunctionTable t. 13 | func (t FunctionTable) Register(name string, fn any) error { 14 | if _, ok := t[name]; ok { 15 | return fmt.Errorf("function '%s' already exists in default table", name) 16 | } 17 | fhirpathFunc, err := ToFunction(fn) 18 | if err != nil { 19 | return err 20 | } 21 | t[name] = fhirpathFunc 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/function_table_test.go: -------------------------------------------------------------------------------- 1 | package funcs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs" 7 | "github.com/verily-src/fhirpath-go/fhirpath/system" 8 | ) 9 | 10 | func TestRegister_RaisesError(t *testing.T) { 11 | testCases := []struct { 12 | name string 13 | funcName string 14 | fn any 15 | }{ 16 | { 17 | name: "raises error when trying to override existing function", 18 | funcName: "where", 19 | fn: func(system.Collection) (system.Collection, error) { return nil, nil }, 20 | }, 21 | { 22 | name: "raises error when adding invalid function", 23 | funcName: "someFn", 24 | fn: func() {}, 25 | }, 26 | } 27 | 28 | for _, tc := range testCases { 29 | t.Run(tc.name, func(t *testing.T) { 30 | table := funcs.Clone() 31 | 32 | if err := table.Register(tc.funcName, tc.fn); err == nil { 33 | t.Fatalf("FunctionTable.Register(%s) doesn't raise error when expected", tc.funcName) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func TestRegister_AddsToMap(t *testing.T) { 40 | table := funcs.Clone() 41 | fn := func(system.Collection) (system.Collection, error) { return nil, nil } 42 | 43 | if err := table.Register("someFn", fn); err != nil { 44 | t.Fatalf("FunctionTable.Register raised unexpected error: %v", err) 45 | } 46 | if _, ok := table["someFn"]; !ok { 47 | t.Errorf("FunctionTable.Register did not successfully add function to map") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/errors.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import "errors" 4 | 5 | // Error constants 6 | var ( 7 | ErrWrongArity = errors.New("incorrect function arity") 8 | ErrInvalidReturnType = errors.New("invalid return type") 9 | ) 10 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/filtering.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 7 | "github.com/verily-src/fhirpath-go/fhirpath/system" 8 | ) 9 | 10 | // Where evaluates the expression args[0] on each input item, collects the items that cause 11 | // the expression to evaluate to true. 12 | func Where(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { 13 | if len(args) != 1 { 14 | return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) 15 | } 16 | e := args[0] 17 | result := system.Collection{} 18 | for _, item := range input { 19 | output, err := e.Evaluate(ctx, system.Collection{item}) 20 | if err != nil { 21 | return nil, err 22 | } 23 | if len(output) == 0 { 24 | continue 25 | } 26 | pass, err := output.ToSingletonBoolean() 27 | if err != nil { 28 | return nil, fmt.Errorf("evaluating where condition as boolean resulted in an error: %w", err) 29 | } 30 | if pass[0] { 31 | result = append(result, item) 32 | } 33 | } 34 | return result, nil 35 | } 36 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/filtering_test.go: -------------------------------------------------------------------------------- 1 | package impl_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 10 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest" 11 | "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" 12 | "github.com/verily-src/fhirpath-go/fhirpath/system" 13 | "github.com/verily-src/fhirpath-go/internal/fhir" 14 | "github.com/verily-src/fhirpath-go/internal/slices" 15 | "google.golang.org/protobuf/testing/protocmp" 16 | ) 17 | 18 | var contact = []*dtpb.ContactDetail{ 19 | { 20 | Name: fhir.String("Vick"), 21 | Id: fhir.String("123"), 22 | }, 23 | { 24 | Name: fhir.String("Vick"), 25 | Id: fhir.String("234"), 26 | }, 27 | { 28 | Name: fhir.String("Matt"), 29 | Id: fhir.String("123"), 30 | }, 31 | } 32 | 33 | func TestWhere_Evaluates(t *testing.T) { 34 | nameEquality := &expr.EqualityExpression{ 35 | Left: &expr.FieldExpression{FieldName: "name"}, 36 | Right: &expr.LiteralExpression{Literal: system.String("Vick")}, 37 | } 38 | 39 | testCases := []struct { 40 | name string 41 | inputCollection system.Collection 42 | inputArgs []expr.Expression 43 | wantCollection system.Collection 44 | }{ 45 | { 46 | name: "filters those that pass name query", 47 | inputCollection: slices.MustConvert[any](contact), 48 | inputArgs: []expr.Expression{nameEquality}, 49 | wantCollection: slices.MustConvert[any](contact[0:2]), 50 | }, 51 | { 52 | name: "passes through when expression evaluates to singleton", 53 | inputCollection: slices.MustConvert[any](contact), 54 | inputArgs: []expr.Expression{exprtest.Return(system.String("1"))}, 55 | wantCollection: slices.MustConvert[any](contact), 56 | }, 57 | { 58 | name: "passes through when expression evaluates to proto boolean true", 59 | inputCollection: slices.MustConvert[any](contact), 60 | inputArgs: []expr.Expression{exprtest.Return(fhir.Boolean(true))}, 61 | wantCollection: slices.MustConvert[any](contact), 62 | }, 63 | { 64 | name: "filters out when expression evaluates to empty", 65 | inputCollection: slices.MustConvert[any](contact), 66 | inputArgs: []expr.Expression{exprtest.Return()}, 67 | wantCollection: system.Collection{}, 68 | }, 69 | } 70 | 71 | for _, tc := range testCases { 72 | t.Run(tc.name, func(t *testing.T) { 73 | got, err := impl.Where(&expr.Context{}, tc.inputCollection, tc.inputArgs...) 74 | if err != nil { 75 | t.Fatalf("Where function returned unexpected error: %v", err) 76 | } 77 | if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { 78 | t.Errorf("Where function returned unexpected diff (-want, +got):\n%s", diff) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestWhere_RaisesError(t *testing.T) { 85 | testCases := []struct { 86 | name string 87 | inputArgs []expr.Expression 88 | inputCollection system.Collection 89 | }{ 90 | { 91 | name: "multiple arguments", 92 | inputArgs: []expr.Expression{exprtest.Return(1), exprtest.Return(1)}, 93 | inputCollection: slices.MustConvert[any](contact), 94 | }, 95 | { 96 | name: "argument expression raises error", 97 | inputArgs: []expr.Expression{exprtest.Error(errors.New("some error"))}, 98 | inputCollection: slices.MustConvert[any](contact), 99 | }, 100 | { 101 | name: "argument expression returns multiple items", 102 | inputArgs: []expr.Expression{exprtest.Return(1, 2)}, 103 | inputCollection: slices.MustConvert[any](contact), 104 | }, 105 | } 106 | 107 | for _, tc := range testCases { 108 | t.Run(tc.name, func(t *testing.T) { 109 | if _, err := impl.Where(&expr.Context{}, tc.inputCollection, tc.inputArgs...); err == nil { 110 | t.Fatalf("evaluating Where function didn't return error when expected") 111 | } 112 | }) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/navigation.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "fmt" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 8 | "github.com/verily-src/fhirpath-go/fhirpath/system" 9 | "github.com/verily-src/fhirpath-go/internal/fhir" 10 | "github.com/verily-src/fhirpath-go/internal/protofields" 11 | ) 12 | 13 | // Children returns a collection with all immediate child nodes of all items in the input collection 14 | // with no specific order. 15 | // FHIRPath docs here: https://hl7.org/fhirpath/N1/#children-collection 16 | func Children(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { 17 | if length := len(args); length != 0 { 18 | return nil, fmt.Errorf("%w, received %v arguments, expected 0", ErrWrongArity, length) 19 | } 20 | 21 | result := system.Collection{} 22 | for _, item := range input { 23 | if system.IsPrimitive(item) { 24 | continue 25 | } 26 | base, ok := item.(fhir.Base) 27 | if !ok { 28 | return nil, fmt.Errorf("%w: unexpected input of type '%T'", ErrInvalidInput, item) 29 | } 30 | if oneOf := protofields.UnwrapOneofField(base, "choice"); oneOf != nil { 31 | if system.IsPrimitive(oneOf) { 32 | continue 33 | } 34 | base = oneOf 35 | } 36 | 37 | var fields []string 38 | if _, ok := base.(*dtpb.Reference); ok { 39 | fields = append(fields, "reference") 40 | } else { 41 | fd := base.ProtoReflect().Descriptor().Fields() 42 | for i := 0; i < fd.Len(); i++ { 43 | fields = append(fields, fd.Get(i).JSONName()) 44 | } 45 | } 46 | for _, f := range fields { 47 | fe := expr.FieldExpression{FieldName: f} 48 | messages, err := fe.Evaluate(ctx, system.Collection{base}) 49 | if err != nil { 50 | return nil, err 51 | } 52 | for _, val := range messages { 53 | if c, ok := val.(system.Collection); ok { 54 | result = append(result, c...) 55 | } else { 56 | result = append(result, val) 57 | } 58 | } 59 | } 60 | } 61 | return result, nil 62 | } 63 | 64 | // Descendants returns a collection with all descendant nodes of all items in the input collection. 65 | // FHIRPath docs here: https://hl7.org/fhirpath/N1/#descendants-collection 66 | func Descendants(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { 67 | if length := len(args); length != 0 { 68 | return nil, fmt.Errorf("%w, received %v arguments, expected 0", ErrWrongArity, length) 69 | } 70 | 71 | result := system.Collection{} 72 | for !input.IsEmpty() { 73 | var err error 74 | input, err = Children(ctx, input) 75 | if err != nil { 76 | return nil, err 77 | } 78 | result = append(result, input...) 79 | } 80 | return result, nil 81 | } 82 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/navigation_test.go: -------------------------------------------------------------------------------- 1 | package impl_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" 7 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 10 | "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" 11 | "github.com/verily-src/fhirpath-go/fhirpath/system" 12 | "github.com/verily-src/fhirpath-go/internal/slices" 13 | "google.golang.org/protobuf/testing/protocmp" 14 | ) 15 | 16 | var names = []*dtpb.HumanName{ 17 | { 18 | Use: &dtpb.HumanName_UseCode{Value: codes_go_proto.NameUseCode_OFFICIAL}, 19 | Family: &dtpb.String{Value: "Smith"}, 20 | Given: []*dtpb.String{ 21 | {Value: "Bob"}, 22 | {Value: "Bobbie"}, 23 | }, 24 | }, 25 | { 26 | Use: &dtpb.HumanName_UseCode{Value: codes_go_proto.NameUseCode_NICKNAME}, 27 | Text: &dtpb.String{Value: "foo bar"}, 28 | Period: &dtpb.Period{ 29 | Start: &dtpb.DateTime{ValueUs: 10}, 30 | End: &dtpb.DateTime{ValueUs: 20}, 31 | }, 32 | }, 33 | } 34 | 35 | func TestChildren_Evaluates(t *testing.T) { 36 | testCases := []struct { 37 | name string 38 | inputCollection system.Collection 39 | wantCollection system.Collection 40 | }{ 41 | { 42 | name: "empty input collection", 43 | inputCollection: system.Collection{}, 44 | wantCollection: system.Collection{}, 45 | }, 46 | { 47 | name: "children of primitives yields empty result", 48 | inputCollection: slices.MustConvert[any](system.Collection{system.Integer(1), system.Boolean(true)}), 49 | wantCollection: system.Collection{}, 50 | }, 51 | { 52 | name: "returns all child nodes", 53 | inputCollection: slices.MustConvert[any](names), 54 | wantCollection: slices.MustConvert[any]( 55 | system.Collection{ 56 | names[0].GetUse(), 57 | names[0].GetFamily(), 58 | names[0].GetGiven()[0], 59 | names[0].GetGiven()[1], 60 | names[1].GetUse(), 61 | names[1].GetText(), 62 | names[1].GetPeriod(), 63 | }, 64 | ), 65 | }, 66 | } 67 | 68 | for _, tc := range testCases { 69 | t.Run(tc.name, func(t *testing.T) { 70 | got, err := impl.Children(&expr.Context{}, tc.inputCollection) 71 | if err != nil { 72 | t.Fatalf("Children function returned unexpected error: %v", err) 73 | } 74 | if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { 75 | t.Errorf("Children function returned unexpected diff (-want, +got):\n%s", diff) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func TestDescendants_Evaluates(t *testing.T) { 82 | testCases := []struct { 83 | name string 84 | inputCollection system.Collection 85 | wantCollection system.Collection 86 | }{ 87 | { 88 | name: "empty input collection", 89 | inputCollection: system.Collection{}, 90 | wantCollection: system.Collection{}, 91 | }, 92 | { 93 | name: "descendants of primitives yields empty result", 94 | inputCollection: slices.MustConvert[any](system.Collection{system.Integer(1), system.Boolean(true)}), 95 | wantCollection: system.Collection{}, 96 | }, 97 | { 98 | name: "returns all descendant nodes", 99 | inputCollection: slices.MustConvert[any](names), 100 | wantCollection: slices.MustConvert[any]( 101 | system.Collection{ 102 | names[0].GetUse(), 103 | names[0].GetFamily(), 104 | names[0].GetGiven()[0], 105 | names[0].GetGiven()[1], 106 | names[1].GetUse(), 107 | names[1].GetText(), 108 | names[1].GetPeriod(), 109 | names[1].GetPeriod().GetStart(), 110 | names[1].GetPeriod().GetEnd(), 111 | }, 112 | ), 113 | }, 114 | } 115 | 116 | for _, tc := range testCases { 117 | t.Run(tc.name, func(t *testing.T) { 118 | got, err := impl.Descendants(&expr.Context{}, tc.inputCollection) 119 | if err != nil { 120 | t.Fatalf("Descendants function returned unexpected error: %v", err) 121 | } 122 | if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { 123 | t.Errorf("Descendants function returned unexpected diff (-want, +got):\n%s", diff) 124 | } 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/not.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 7 | "github.com/verily-src/fhirpath-go/fhirpath/system" 8 | ) 9 | 10 | // Not returns the boolean inverse of the singleton input collection. 11 | func Not(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { 12 | if length := len(args); length != 0 { 13 | return nil, fmt.Errorf("%w, received %v arguments, expected 0", ErrWrongArity, length) 14 | } 15 | 16 | boolean, err := input.ToSingletonBoolean() 17 | if err != nil { 18 | return nil, err 19 | } 20 | if len(boolean) == 0 { 21 | return system.Collection{}, nil 22 | } 23 | result := system.Boolean(!boolean[0]) 24 | return system.Collection{result}, nil 25 | } 26 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/not_test.go: -------------------------------------------------------------------------------- 1 | package impl_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 8 | "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" 9 | "github.com/verily-src/fhirpath-go/fhirpath/system" 10 | "github.com/verily-src/fhirpath-go/internal/fhir" 11 | ) 12 | 13 | func TestNot_InvertsBoolean(t *testing.T) { 14 | testCases := []struct { 15 | name string 16 | input system.Collection 17 | want system.Collection 18 | wantErr bool 19 | }{ 20 | { 21 | name: "inverts system boolean", 22 | input: system.Collection{system.Boolean(true)}, 23 | want: system.Collection{system.Boolean(false)}, 24 | }, 25 | { 26 | name: "inverts proto boolean", 27 | input: system.Collection{fhir.Boolean(false)}, 28 | want: system.Collection{system.Boolean(true)}, 29 | }, 30 | { 31 | name: "receives non-singleton collection", 32 | input: system.Collection{system.Boolean(true), system.Boolean(false)}, 33 | wantErr: true, 34 | }, 35 | { 36 | name: "passes through empty collection", 37 | input: system.Collection{}, 38 | want: system.Collection{}, 39 | }, 40 | } 41 | 42 | for _, tc := range testCases { 43 | t.Run(tc.name, func(t *testing.T) { 44 | got, err := impl.Not(&expr.Context{}, tc.input) 45 | 46 | if gotErr := err != nil; tc.wantErr != gotErr { 47 | t.Fatalf("Not function got unexpected error result: gotErr %v, wantErr %v, err: %v", gotErr, tc.wantErr, err) 48 | } 49 | if !cmp.Equal(tc.want, got) { 50 | t.Errorf("Not function returned unexpected result: got: %v, want %v", got, tc.want) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/projection.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 8 | "github.com/verily-src/fhirpath-go/fhirpath/system" 9 | ) 10 | 11 | // Select evaluates the expression args[0] on each input item. The result of each evaluation is 12 | // added to the output collection. 13 | // FHIRPath docs here: https://hl7.org/fhirpath/N1/#selectprojection-expression-collection 14 | func Select(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { 15 | if len(args) != 1 { 16 | return nil, fmt.Errorf("%w: received %v arguments, expected 1", ErrWrongArity, len(args)) 17 | } 18 | e := args[0] 19 | result := system.Collection{} 20 | var fieldErrs []error 21 | for _, item := range input { 22 | output, err := e.Evaluate(ctx, system.Collection{item}) 23 | // If the error is ErrInvalidField, don't immediately raise it 24 | if err != nil { 25 | if errors.Is(err, expr.ErrInvalidField) { 26 | fieldErrs = append(fieldErrs, err) 27 | continue 28 | } 29 | return nil, err 30 | } 31 | result = append(result, output...) 32 | } 33 | // Raise field errors if one was raised for each input. 34 | if len(input) > 0 && len(fieldErrs) == len(input) { 35 | return nil, errors.Join(fieldErrs...) 36 | } 37 | return result, nil 38 | } 39 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/projection_test.go: -------------------------------------------------------------------------------- 1 | package impl_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 10 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest" 11 | "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" 12 | "github.com/verily-src/fhirpath-go/fhirpath/system" 13 | "github.com/verily-src/fhirpath-go/internal/fhir" 14 | "github.com/verily-src/fhirpath-go/internal/slices" 15 | "google.golang.org/protobuf/testing/protocmp" 16 | ) 17 | 18 | var address = []*dtpb.Address{ 19 | { 20 | Line: []*dtpb.String{ 21 | fhir.String("123 Main St"), 22 | fhir.String("Apt 1"), 23 | }, 24 | State: fhir.String("CA"), 25 | }, 26 | { 27 | Line: []*dtpb.String{ 28 | fhir.String("456 Main St"), 29 | fhir.String("Apt 2"), 30 | }, 31 | State: fhir.String("TX"), 32 | }, 33 | } 34 | 35 | func TestSelect_Evaluates(t *testing.T) { 36 | testCases := []struct { 37 | name string 38 | inputCollection system.Collection 39 | inputArgs []expr.Expression 40 | wantCollection system.Collection 41 | }{ 42 | { 43 | name: "projection on empty collection", 44 | inputCollection: system.Collection{}, 45 | inputArgs: []expr.Expression{exprtest.Return(system.Boolean(true))}, 46 | wantCollection: system.Collection{}, 47 | }, 48 | { 49 | name: "projection yields empty result", 50 | inputCollection: slices.MustConvert[any](address), 51 | inputArgs: []expr.Expression{exprtest.Return()}, 52 | wantCollection: system.Collection{}, 53 | }, 54 | { 55 | name: "project state field", 56 | inputCollection: slices.MustConvert[any](address), 57 | inputArgs: []expr.Expression{&expr.FieldExpression{FieldName: "state"}}, 58 | wantCollection: system.Collection{address[0].GetState(), address[1].GetState()}, 59 | }, 60 | { 61 | name: "projection flattens output collections", 62 | inputCollection: slices.MustConvert[any](address), 63 | inputArgs: []expr.Expression{&expr.FieldExpression{FieldName: "line"}}, 64 | wantCollection: system.Collection{ 65 | address[0].GetLine()[0], 66 | address[0].GetLine()[1], 67 | address[1].GetLine()[0], 68 | address[1].GetLine()[1], 69 | }, 70 | }, 71 | { 72 | name: "does not raise error if field is valid for at least one input", 73 | inputCollection: system.Collection{address[0], fhir.String("string")}, 74 | inputArgs: []expr.Expression{&expr.FieldExpression{FieldName: "state"}}, 75 | wantCollection: system.Collection{address[0].GetState()}, 76 | }, 77 | } 78 | 79 | for _, tc := range testCases { 80 | t.Run(tc.name, func(t *testing.T) { 81 | got, err := impl.Select(&expr.Context{}, tc.inputCollection, tc.inputArgs...) 82 | if err != nil { 83 | t.Fatalf("Select function returned unexpected error: %v", err) 84 | } 85 | if diff := cmp.Diff(tc.wantCollection, got, protocmp.Transform()); diff != "" { 86 | t.Errorf("Select function returned unexpected diff (-want, +got):\n%s", diff) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func TestSelect_RaisesError(t *testing.T) { 93 | testCases := []struct { 94 | name string 95 | inputArgs []expr.Expression 96 | inputCollection system.Collection 97 | }{ 98 | { 99 | name: "multiple arguments", 100 | inputArgs: []expr.Expression{exprtest.Return(1), exprtest.Return(1)}, 101 | inputCollection: slices.MustConvert[any](address), 102 | }, 103 | { 104 | name: "argument expression raises error", 105 | inputArgs: []expr.Expression{exprtest.Error(errors.New("some error"))}, 106 | inputCollection: slices.MustConvert[any](address), 107 | }, 108 | { 109 | name: "invalid field as argument expression", 110 | inputArgs: []expr.Expression{&expr.FieldExpression{FieldName: "invalid"}}, 111 | inputCollection: slices.MustConvert[any](address), 112 | }, 113 | } 114 | 115 | for _, tc := range testCases { 116 | t.Run(tc.name, func(t *testing.T) { 117 | if _, err := impl.Select(&expr.Context{}, tc.inputCollection, tc.inputArgs...); err == nil { 118 | t.Fatalf("evaluating Select function didn't return error when expected") 119 | } 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/r4.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 7 | "github.com/verily-src/fhirpath-go/fhirpath/system" 8 | "github.com/verily-src/fhirpath-go/internal/fhir" 9 | ) 10 | 11 | // Extension is syntactic sugar over `extension.where(url = ...)`, and is 12 | // specific to the R4 extensions for FHIRPath (as oppose to being part of the 13 | // N1 normative spec). 14 | // 15 | // For more details, see https://hl7.org/fhir/R4/fhirpath.html#functions 16 | func Extension(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { 17 | if len(args) != 1 { 18 | return nil, fmt.Errorf("%w: expected 1 argument", ErrWrongArity) 19 | } 20 | arg, err := args[0].Evaluate(ctx, input) 21 | if err != nil { 22 | return nil, err 23 | } 24 | str, err := arg.ToString() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | var result system.Collection 30 | for _, entry := range input { 31 | entry, ok := entry.(fhir.Extendable) 32 | if !ok { 33 | continue 34 | } 35 | for _, ext := range entry.GetExtension() { 36 | if url := ext.GetUrl(); url != nil && url.Value == str { 37 | result = append(result, ext) 38 | } 39 | } 40 | } 41 | return result, nil 42 | } 43 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/r4_test.go: -------------------------------------------------------------------------------- 1 | package impl_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 8 | ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/google/go-cmp/cmp/cmpopts" 11 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 12 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr/exprtest" 13 | "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" 14 | "github.com/verily-src/fhirpath-go/fhirpath/system" 15 | "github.com/verily-src/fhirpath-go/internal/element/extension" 16 | "github.com/verily-src/fhirpath-go/internal/fhir" 17 | "github.com/verily-src/fhirpath-go/internal/fhirtest" 18 | "google.golang.org/protobuf/testing/protocmp" 19 | ) 20 | 21 | func TestExtension_ValidInput(t *testing.T) { 22 | extURL := "1234" 23 | patient := fhirtest.NewResourceOf[*ppb.Patient](t) 24 | ext := extension.New(extURL, fhir.String("some value")) 25 | patient.Extension = append(patient.Extension, &dtpb.Extension{}) 26 | patient.Extension = append(patient.Extension, ext, ext) 27 | testCases := []struct { 28 | name string 29 | input system.Collection 30 | arg string 31 | want system.Collection 32 | }{ 33 | { 34 | name: "empty input result in empty output", 35 | input: system.Collection{}, 36 | arg: "some-url", 37 | want: nil, 38 | }, 39 | { 40 | name: "entries have extensions not matched by url", 41 | input: system.Collection{ 42 | fhirtest.NewResourceOf[*ppb.Patient](t), 43 | }, 44 | arg: "some-url", 45 | want: nil, 46 | }, 47 | { 48 | name: "input does not have extension field", 49 | input: system.Collection{ 50 | system.String("hello world"), 51 | }, 52 | arg: "some-url", 53 | want: nil, 54 | }, 55 | { 56 | name: "entries have extensions matched by url", 57 | input: system.Collection{patient}, 58 | arg: extURL, 59 | want: system.Collection{ext, ext}, 60 | }, 61 | } 62 | 63 | for _, tc := range testCases { 64 | t.Run(tc.name, func(t *testing.T) { 65 | got, err := impl.Extension(&expr.Context{}, tc.input, exprtest.Return(system.String(tc.arg))) 66 | if err != nil { 67 | t.Fatalf("Extension function returned unexpected error: %v", err) 68 | } 69 | 70 | if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 71 | t.Errorf("Extension function returned unexpected diff (-want, +got):\n%s", diff) 72 | } 73 | }) 74 | } 75 | } 76 | 77 | func TestExtension_InvalidInput_RaisesError(t *testing.T) { 78 | testErr := errors.New("test error") 79 | testCases := []struct { 80 | name string 81 | input system.Collection 82 | args []expr.Expression 83 | wantErr error 84 | }{ 85 | { 86 | name: "too many arguments", 87 | input: system.Collection{}, 88 | args: []expr.Expression{ 89 | exprtest.Return(system.String("")), 90 | exprtest.Return(system.String("")), 91 | }, 92 | wantErr: impl.ErrWrongArity, 93 | }, { 94 | name: "too few arguments", 95 | input: system.Collection{}, 96 | args: []expr.Expression{}, 97 | wantErr: impl.ErrWrongArity, 98 | }, { 99 | name: "invalid argument type", 100 | input: system.Collection{}, 101 | args: []expr.Expression{exprtest.Return(system.Integer(42))}, 102 | wantErr: cmpopts.AnyError, 103 | }, { 104 | name: "argument errors", 105 | input: system.Collection{}, 106 | args: []expr.Expression{exprtest.Error(testErr)}, 107 | wantErr: testErr, 108 | }, 109 | } 110 | 111 | for _, tc := range testCases { 112 | t.Run(tc.name, func(t *testing.T) { 113 | _, err := impl.Extension(&expr.Context{}, tc.input, tc.args...) 114 | 115 | if got, want := err, tc.wantErr; !cmp.Equal(got, want, cmpopts.EquateErrors()) { 116 | t.Fatalf("Extension(%v): got err %v, want err %v", tc.name, got, want) 117 | } 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/utility.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 5 | "github.com/verily-src/fhirpath-go/fhirpath/system" 6 | ) 7 | 8 | // TimeOfDay returns the current time as a system.Time object. 9 | func TimeOfDay(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { 10 | timeString := ctx.Now.Format("15:04:05.000") 11 | return system.Collection{system.MustParseTime(timeString)}, nil 12 | } 13 | 14 | // Today returns the current date as a system.Date object. 15 | func Today(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { 16 | dateString := ctx.Now.Format("2006-01-02") 17 | return system.Collection{system.MustParseDate(dateString)}, nil 18 | } 19 | 20 | // Now returns the current time as a system.DateTime object. 21 | func Now(ctx *expr.Context, input system.Collection, args ...expr.Expression) (system.Collection, error) { 22 | dateTimeString := ctx.Now.Format("2006-01-02T15:04:05.000Z07:00") 23 | return system.Collection{system.MustParseDateTime(dateTimeString)}, nil 24 | } 25 | -------------------------------------------------------------------------------- /fhirpath/internal/funcs/impl/utility_test.go: -------------------------------------------------------------------------------- 1 | package impl_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 9 | "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs/impl" 10 | "github.com/verily-src/fhirpath-go/fhirpath/system" 11 | ) 12 | 13 | func TestTimeOfDay(t *testing.T) { 14 | ctx := &expr.Context{Now: time.Date(0, time.January, 1, 19, 30, 5, 1000000, time.UTC)} 15 | wantCollection := system.Collection{system.MustParseTime("19:30:05.001")} 16 | 17 | got, err := impl.TimeOfDay(ctx, []any{}) 18 | if err != nil { 19 | t.Fatalf("impl.TimeOfDay() returned unexpected error: %v", err) 20 | } 21 | if !cmp.Equal(got, wantCollection) { 22 | t.Errorf("impl.TimeOfDay() returned unexpected result: got %v, want %v", got, wantCollection) 23 | } 24 | } 25 | 26 | func TestToday(t *testing.T) { 27 | ctx := &expr.Context{Now: time.Date(2010, time.February, 12, 0, 0, 0, 0, time.UTC)} 28 | wantCollection := system.Collection{system.MustParseDate("2010-02-12")} 29 | 30 | got, err := impl.Today(ctx, []any{}) 31 | if err != nil { 32 | t.Fatalf("impl.Today() returned unexpected error: %v", err) 33 | } 34 | if !cmp.Equal(got, wantCollection) { 35 | t.Errorf("impl.Today() returned unexpected result: got %v, want %v", got, wantCollection) 36 | } 37 | } 38 | 39 | func TestNow(t *testing.T) { 40 | ctx := &expr.Context{Now: time.Date(2010, time.February, 12, 12, 30, 34, 2000000, time.UTC)} 41 | wantCollection := system.Collection{system.MustParseDateTime("2010-02-12T12:30:34.002Z")} 42 | 43 | got, err := impl.Now(ctx, []any{}) 44 | if err != nil { 45 | t.Fatalf("impl.Now() returned unexpected error: %v", err) 46 | } 47 | if !cmp.Equal(got, wantCollection) { 48 | t.Errorf("impl.Now() returned unexpected result: got %v, want %v", got, wantCollection) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /fhirpath/internal/grammar/generate.go: -------------------------------------------------------------------------------- 1 | package grammar 2 | 3 | //go:generate ./generate.sh 4 | -------------------------------------------------------------------------------- /fhirpath/internal/grammar/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | shopt -s expand_aliases 4 | 5 | antlr_jar=antlr-4.13.0-complete.jar 6 | 7 | if [ ! -f ${antlr_jar} ]; then 8 | wget "https://www.antlr.org/download/${antlr_jar}" 9 | fi 10 | 11 | alias antlr4="java -Xmx500M -cp './$antlr_jar:\$CLASSPATH' org.antlr.v4.Tool" 12 | antlr4 -Dlanguage=Go -no-listener -visitor -package grammar *.g4 13 | -------------------------------------------------------------------------------- /fhirpath/internal/opts/opts.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package opts is an internal package that exists for setting configuration 3 | settings for FHIRPath. This is an internal package so that only parts of this 4 | may be publicly re-exported, while the implementation has access to the full 5 | thing. 6 | */ 7 | package opts 8 | 9 | import ( 10 | "errors" 11 | 12 | "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 13 | "github.com/verily-src/fhirpath-go/fhirpath/internal/funcs" 14 | "github.com/verily-src/fhirpath-go/fhirpath/internal/parser" 15 | ) 16 | 17 | // CompileConfig provides the configuration values for the Compile command. 18 | type CompileConfig struct { 19 | // Table is the current function table to be called. 20 | Table funcs.FunctionTable 21 | Transform parser.VisitorTransform 22 | 23 | // Permissive is a legacy option to allow FHIRpaths with *invalid* fields to be 24 | // compiled (to reduce breakages). 25 | Permissive bool 26 | } 27 | 28 | // EvaluateConfig provides the configuration values for the Evaluate command. 29 | type EvaluateConfig struct { 30 | // Context is the current context information. 31 | Context *expr.Context 32 | } 33 | 34 | // Option is the base interface for FHIRPath options. 35 | type Option[T any] interface { 36 | updateConfig(*T) error 37 | } 38 | 39 | // CompileOption is an Option that sets CompileConfig. 40 | type CompileOption = Option[CompileConfig] 41 | 42 | // EvaluateOption is an Option that sets EvaluateConfig. 43 | type EvaluateOption = Option[EvaluateConfig] 44 | 45 | // Transform creates either an Evaluate or Compile configuration option, done 46 | // as a function callback. 47 | func Transform[T any](callback func(cfg *T) error) Option[T] { 48 | return callbackOption[T]{callback: callback} 49 | } 50 | 51 | // ApplyOptions applies all the options to the given configuration. 52 | func ApplyOptions[T any](cfg *T, opts ...Option[T]) (*T, error) { 53 | var errs []error 54 | for _, opt := range opts { 55 | errs = append(errs, opt.updateConfig(cfg)) 56 | } 57 | return cfg, errors.Join(errs...) 58 | } 59 | 60 | type callbackOption[T any] struct { 61 | callback func(*T) error 62 | } 63 | 64 | func (o callbackOption[T]) updateConfig(cfg *T) error { 65 | return o.callback(cfg) 66 | } 67 | -------------------------------------------------------------------------------- /fhirpath/internal/parser/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package parser provides the logic for traversing 3 | the ANTLR generated parse tree. Provides a visitor 4 | and related functions. 5 | */ 6 | package parser 7 | -------------------------------------------------------------------------------- /fhirpath/internal/parser/error_handling.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/antlr4-go/antlr/v4" 8 | ) 9 | 10 | type FHIRPathErrorListener struct { 11 | *antlr.DefaultErrorListener 12 | errors []error 13 | } 14 | 15 | func (l *FHIRPathErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) { 16 | err := fmt.Errorf("syntax error on line %d:%d - %s", line, column, msg) 17 | l.errors = append(l.errors, err) 18 | } 19 | 20 | func (l *FHIRPathErrorListener) Error() error { 21 | return errors.Join(l.errors...) 22 | } 23 | -------------------------------------------------------------------------------- /fhirpath/internal/parser/transforms.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "github.com/verily-src/fhirpath-go/fhirpath/internal/expr" 4 | 5 | // A VisitorTransform is a function which transforms the specified 6 | // expression. This is used in FHIRPath Patch to modify expressions. 7 | type VisitorTransform func(expr.Expression) expr.Expression 8 | 9 | // IdentityTransform returns the given expression without any modification. 10 | func IdentityTransform(e expr.Expression) expr.Expression { 11 | return e 12 | } 13 | -------------------------------------------------------------------------------- /fhirpath/internal/reflection/consts.go: -------------------------------------------------------------------------------- 1 | package reflection 2 | 3 | // Valid namespace constants. 4 | const ( 5 | FHIR = "FHIR" 6 | System = "System" 7 | ) 8 | -------------------------------------------------------------------------------- /fhirpath/internal/reflection/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package reflection provides types and utility functions 3 | to enable FHIRPath type reflection. 4 | */ 5 | package reflection 6 | -------------------------------------------------------------------------------- /fhirpath/internal/reflection/elements.go: -------------------------------------------------------------------------------- 1 | package reflection 2 | 3 | import ( 4 | "github.com/iancoleman/strcase" 5 | "github.com/verily-src/fhirpath-go/internal/protofields" 6 | ) 7 | 8 | // IsValidFHIRPathElement checks if the input string represents 9 | // a valid element name. This function is importantly case-sensitive, 10 | // which is a distinction that is important for primitive types. 11 | func IsValidFHIRPathElement(name string) bool { 12 | if isPrimitive(name) || name == "BackboneElement" { 13 | return true 14 | } 15 | return protofields.IsValidElementType(primitiveToLowercase(name)) 16 | } 17 | 18 | func isPrimitive(name string) bool { 19 | switch name { 20 | case "instant", "time", "date", "dateTime", "base64Binary", 21 | "decimal", "boolean", "url", "code", "string", "integer", "uri", 22 | "canonical", "markdown", "id", "oid", "uuid", "unsignedInt", "positiveInt": 23 | return true 24 | default: 25 | return false 26 | } 27 | } 28 | 29 | func primitiveToLowercase(name string) string { 30 | switch name { 31 | case "Instant", "Time", "Date", "DateTime", "Base64Binary", 32 | "Decimal", "Boolean", "Url", "Code", "String", "Integer", "Uri", 33 | "Canonical", "Markdown", "Id", "Oid", "Uuid", "UnsignedInt", "PositiveInt": 34 | return strcase.ToLowerCamel(name) 35 | default: 36 | return name 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /fhirpath/internal/reflection/type_specifier.go: -------------------------------------------------------------------------------- 1 | package reflection 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/verily-src/fhirpath-go/fhirpath/system" 8 | "github.com/verily-src/fhirpath-go/internal/fhir" 9 | "github.com/verily-src/fhirpath-go/internal/protofields" 10 | ) 11 | 12 | var ( 13 | errInvalidType = errors.New("invalid type name") 14 | errInvalidNamespace = errors.New("invalid namespace") 15 | errInvalidInput = errors.New("invalid input type") 16 | ) 17 | 18 | // TypeSpecifier is a FHIRPath type that enables use of 19 | // is and as operators. Provides a namespace and type name 20 | type TypeSpecifier struct { 21 | namespace string 22 | typeName string 23 | } 24 | 25 | // NewQualifiedTypeSpecifier constructs a Qualified Type Specifier given a namespace and typeName. 26 | // Returns an error if the typeName is not found within the namespace, or the namespace is invalid. 27 | func NewQualifiedTypeSpecifier(namespace string, typeName string) (TypeSpecifier, error) { 28 | switch namespace { 29 | case FHIR: 30 | if IsValidFHIRPathElement(typeName) || protofields.IsValidResourceType(typeName) || isBaseType(typeName) { 31 | return TypeSpecifier{namespace: namespace, typeName: typeName}, nil 32 | } 33 | return TypeSpecifier{}, fmt.Errorf("%w: %s", errInvalidType, typeName) 34 | case System: 35 | if system.IsValid(typeName) { 36 | return TypeSpecifier{namespace: namespace, typeName: typeName}, nil 37 | } 38 | return TypeSpecifier{}, fmt.Errorf("%w: %s", errInvalidType, typeName) 39 | default: 40 | return TypeSpecifier{}, fmt.Errorf("%w: %s", errInvalidNamespace, namespace) 41 | } 42 | } 43 | 44 | // NewTypeSpecifier constructs a Qualified Type Specifier given a typeName. The namespace 45 | // is inferred with the priority rules of FHIRPath. Returns an error if the typeName cannot 46 | // be resolved. 47 | func NewTypeSpecifier(typeName string) (TypeSpecifier, error) { 48 | if IsValidFHIRPathElement(typeName) || protofields.IsValidResourceType(typeName) || isBaseType(typeName) { 49 | return TypeSpecifier{FHIR, typeName}, nil 50 | } 51 | if system.IsValid(typeName) { 52 | return TypeSpecifier{System, typeName}, nil 53 | } 54 | return TypeSpecifier{}, fmt.Errorf("%w: %s", errInvalidType, typeName) 55 | } 56 | 57 | // TypeOf retrieves the Type Specifier of the input, given that it is 58 | // a supported FHIRPath type. Otherwise, returns an error. 59 | func TypeOf(input any) (TypeSpecifier, error) { 60 | if item, ok := input.(system.Any); ok { 61 | return TypeSpecifier{System, item.Name()}, nil 62 | } 63 | item, ok := input.(fhir.Base) 64 | if !ok { 65 | return TypeSpecifier{}, fmt.Errorf("%w: no type specifier available", errInvalidInput) 66 | } 67 | if oneOf := protofields.UnwrapOneofField(item, "choice"); oneOf != nil { 68 | item = oneOf 69 | } 70 | name := string(item.ProtoReflect().Descriptor().Name()) 71 | if protofields.IsCodeField(item) { 72 | return TypeSpecifier{FHIR, "code"}, nil 73 | } 74 | return TypeSpecifier{FHIR, primitiveToLowercase(name)}, nil 75 | } 76 | 77 | // Is returns a boolean representing whether or not the receiver type is equivalent to the 78 | // input type, or if it's a valid subtype. 79 | func (ts TypeSpecifier) Is(input TypeSpecifier) system.Boolean { 80 | if ts.namespace != input.namespace { 81 | return false 82 | } 83 | // If the root type has been reached and the equality is still false, they are not equal 84 | if ts == ts.parent() && ts.typeName != input.typeName { 85 | return false 86 | } 87 | if ts.typeName == input.typeName { 88 | return true 89 | } 90 | return ts.parent().Is(input) // Recursively compare the parent type 91 | } 92 | 93 | // MustCreateTypeSpecifier creates a qualified type specifier and panics if the 94 | // provided namespace or typeName is invalid. Returns the created TypeSpecifier 95 | func MustCreateTypeSpecifier(namespace string, typeName string) TypeSpecifier { 96 | typeSpecifier, err := NewQualifiedTypeSpecifier(namespace, typeName) 97 | if err != nil { 98 | panic(err) 99 | } 100 | return typeSpecifier 101 | } 102 | 103 | func (ts TypeSpecifier) parent() TypeSpecifier { 104 | if ts.namespace == System { 105 | return TypeSpecifier{"System", "Any"} 106 | } 107 | 108 | switch ts.typeName { 109 | case "code", "markdown", "id": 110 | return TypeSpecifier{ts.namespace, "string"} 111 | case "unsignedInt", "positiveInt": 112 | return TypeSpecifier{ts.namespace, "integer"} 113 | case "url", "canonical", "uuid", "oid": 114 | return TypeSpecifier{ts.namespace, "uri"} 115 | case "Duration", "MoneyQuantity", "Age", "Count", "Distance", "SimpleQuantity": 116 | return TypeSpecifier{ts.namespace, "Quantity"} 117 | case "Timing", "Dosage", "ElementDefinition": 118 | return TypeSpecifier{ts.namespace, "BackboneElement"} 119 | case "Bundle", "Binary", "Parameters", "DomainResource": 120 | return TypeSpecifier{ts.namespace, "Resource"} 121 | case "Element", "Resource": 122 | return ts 123 | default: 124 | if IsValidFHIRPathElement(ts.typeName) { 125 | return TypeSpecifier{ts.namespace, "Element"} 126 | } 127 | return TypeSpecifier{ts.namespace, "DomainResource"} 128 | } 129 | } 130 | 131 | func isBaseType(name string) bool { 132 | switch name { 133 | case "Element", "Resource", "DomainResource": 134 | return true 135 | } 136 | return false 137 | } 138 | -------------------------------------------------------------------------------- /fhirpath/options.go: -------------------------------------------------------------------------------- 1 | package fhirpath 2 | 3 | import ( 4 | "github.com/verily-src/fhirpath-go/fhirpath/compopts" 5 | "github.com/verily-src/fhirpath-go/fhirpath/evalopts" 6 | "github.com/verily-src/fhirpath-go/fhirpath/internal/opts" 7 | ) 8 | 9 | // CompileOption is a function type that modifies a passed in compileOption. 10 | // Can define mutator functions of this type (see WithLimitation below) 11 | type CompileOption = opts.CompileOption 12 | 13 | // EvaluateOption is a function type that mutates the evalOptions type. 14 | type EvaluateOption = opts.EvaluateOption 15 | 16 | // WithFunction is a compile option that allows the addition of user-defined 17 | // functions to a FHIRPath expression. Function argument must match the signature 18 | // func(Collection, ...any) (Collection, error), or an error will be raised. 19 | // 20 | // Deprecated: Use compopts.Function instead. 21 | func WithFunction(name string, fn any) CompileOption { 22 | return compopts.AddFunction(name, fn) 23 | } 24 | 25 | // WithConstant is an EvaluateOption that allows the addition of external 26 | // constant variables. An error will be raised if the value passed in is 27 | // neither a fhir proto or system type. 28 | // 29 | // Deprecated: Use evalopts.EnvVariable instead 30 | func WithConstant(name string, value any) EvaluateOption { 31 | return evalopts.EnvVariable(name, value) 32 | } 33 | -------------------------------------------------------------------------------- /fhirpath/patch/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package patch implements the FHIRPath Patch specification. 3 | 4 | More documentation about the specification can be found on HL7: 5 | https://hl7.org/fhir/R4/fhirpatch.html#3.1.5. 6 | */ 7 | package patch 8 | -------------------------------------------------------------------------------- /fhirpath/system/cmp.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import "reflect" 4 | 5 | // Equal compares two FHIRPath System types for equality. This uses standard 6 | // equality semantics and will return true if the value should yield a value 7 | // that is true, and false otherwise. 8 | // 9 | // This is effectively sugar over calling: 10 | // 11 | // result, ok := TryEqual(lhs, rhs) 12 | // return result && ok 13 | // 14 | // See https://hl7.org/fhirpath/n1/#equality 15 | func Equal(lhs, rhs Any) bool { 16 | result, ok := TryEqual(lhs, rhs) 17 | return ok && result 18 | } 19 | 20 | // TryEqual compares two FHIRPath System types for equality. This returns a 21 | // value if and only if the comparison of the underlying System types should 22 | // also yield a value, as defined in FHIRPath's equality operation. 23 | // 24 | // See https://hl7.org/fhirpath/n1/#equality 25 | // 26 | // For system types that define a custom "Equal" function, this will call the 27 | // underlying function. For system types that define a custom "TryEqual" function 28 | // this will call the underlying function. Otherwise, this will compare the raw 29 | // representation instead. 30 | func TryEqual(lhs, rhs Any) (bool, bool) { 31 | lhs, rhs = Normalize(lhs, rhs), Normalize(rhs, lhs) 32 | if result, has, ok := callTryEqual(lhs, rhs); ok { 33 | return result, has 34 | } 35 | if result, ok := callEqual(lhs, rhs); ok { 36 | return result, true 37 | } 38 | return lhs == rhs, true 39 | } 40 | 41 | func callTryEqual(lhs, rhs Any) (bool, bool, bool) { 42 | if eq, ok := reflect.TypeOf(lhs).MethodByName("TryEqual"); ok { 43 | funcType := eq.Func.Type() 44 | arg0, arg1 := funcType.In(0), funcType.In(1) 45 | if arg0 != reflect.TypeOf(lhs) || arg1.ConvertibleTo(reflect.TypeOf(rhs)) { 46 | return false, false, true 47 | } 48 | args := []reflect.Value{ 49 | reflect.ValueOf(lhs), 50 | reflect.ValueOf(rhs), 51 | } 52 | result := eq.Func.Call(args) 53 | return result[0].Bool(), result[1].Bool(), true 54 | } 55 | return false, false, false 56 | } 57 | 58 | func callEqual(lhs, rhs Any) (bool, bool) { 59 | if got, ok := callBinaryComparator("Equal", lhs, rhs); ok { 60 | return got.(bool), true 61 | } 62 | return false, false 63 | } 64 | 65 | // callBinaryComparator invokes a binary comparator operator 66 | func callBinaryComparator(name string, lhs, rhs any) (any, bool) { 67 | if eq, ok := reflect.TypeOf(lhs).MethodByName(name); ok { 68 | args := []reflect.Value{ 69 | reflect.ValueOf(lhs), 70 | reflect.ValueOf(rhs), 71 | } 72 | funcType := eq.Func.Type() 73 | arg0, arg1 := funcType.In(0), funcType.In(1) 74 | if arg0 != reflect.TypeOf(lhs) || arg1.ConvertibleTo(reflect.TypeOf(rhs)) { 75 | return nil, true 76 | } 77 | result := eq.Func.Call(args) 78 | return result[0].Bool(), true 79 | } 80 | return nil, false 81 | } 82 | -------------------------------------------------------------------------------- /fhirpath/system/consts.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import "errors" 4 | 5 | // Common errors. 6 | var ( 7 | ErrTypeMismatch = errors.New("operation not defined between given types") 8 | // Date, Time, DateTime, and Quantity have special cases for equality and inequality logic 9 | // where an empty collection should be returned when their precisions/units are mismatched. 10 | ErrMismatchedPrecision = errors.New("mismatched precision") 11 | ErrMismatchedUnit = errors.New("mismatched unit") 12 | ErrIntOverflow = errors.New("operation resulted in integer overflow") 13 | ) 14 | 15 | // Type names. 16 | const ( 17 | stringType = "String" 18 | booleanType = "Boolean" 19 | integerType = "Integer" 20 | decimalType = "Decimal" 21 | dateType = "Date" 22 | dateTimeType = "DateTime" 23 | timeType = "Time" 24 | quantityType = "Quantity" 25 | anyType = "Any" 26 | ) 27 | -------------------------------------------------------------------------------- /fhirpath/system/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package system provides types and related utility functions 3 | for all the valid FHIRPath System types to be used for literals. 4 | 5 | More documentation about FHIRPath System types can be found here: 6 | - http://hl7.org/fhirpath/N1/#literals 7 | */ 8 | package system 9 | -------------------------------------------------------------------------------- /fhirpath/system/layouts.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | // datePrecision enumerates date precision constants. 4 | type datePrecision int 5 | 6 | // timePrecision enumerates time precision constants. 7 | type timePrecision int 8 | 9 | // dateTimePrecision enumerates dateTime precision constants. 10 | type dateTimePrecision int 11 | 12 | // layout represents a layout string for parsing. 13 | type layout string 14 | 15 | // Date precision constants. 16 | const ( 17 | year datePrecision = iota 18 | month 19 | day 20 | ) 21 | 22 | // Time precision constants. 23 | const ( 24 | hour timePrecision = iota 25 | minute 26 | second 27 | ) 28 | 29 | // DateTime precision constants. 30 | const ( 31 | dtYear dateTimePrecision = iota 32 | dtMonth 33 | dtDay 34 | dtHour 35 | dtMinute 36 | dtSecond 37 | ) 38 | 39 | // layout constants. 40 | const ( 41 | yearLayout = "2006" 42 | monthLayout = "2006-01" 43 | dayLayout = "2006-01-02" 44 | hourLayout = "15" 45 | minuteLayout = "15:04" 46 | secondLayout = "15:04:05" 47 | millisecondLayout = "15:04:05.000" 48 | dtMillisecondLayoutTZ = "2006-01-02T15:04:05.000Z07:00" 49 | dtMillisecondLayout = "2006-01-02T15:04:05.000" 50 | dtSecondLayoutTZ = "2006-01-02T15:04:05Z07:00" 51 | dtSecondLayout = "2006-01-02T15:04:05" 52 | dtMinuteLayoutTZ = "2006-01-02T15:04Z07:00" 53 | dtMinuteLayout = "2006-01-02T15:04" 54 | dtHourLayoutTZ = "2006-01-02T15Z07:00" 55 | dtHourLayout = "2006-01-02T15" 56 | dtDayLayout = "2006-01-02T" 57 | dtMonthLayout = "2006-01T" 58 | dtYearLayout = "2006T" 59 | ) 60 | 61 | var dateMap = map[layout]datePrecision{ 62 | dayLayout: day, 63 | monthLayout: month, 64 | yearLayout: year, 65 | } 66 | 67 | var timeMap = map[layout]timePrecision{ 68 | millisecondLayout: second, 69 | secondLayout: second, 70 | minuteLayout: minute, 71 | hourLayout: hour, 72 | } 73 | 74 | var dateTimeMap = map[layout]dateTimePrecision{ 75 | dtMillisecondLayoutTZ: dtSecond, 76 | dtMillisecondLayout: dtSecond, 77 | dtSecondLayoutTZ: dtSecond, 78 | dtSecondLayout: dtSecond, 79 | dtMinuteLayoutTZ: dtMinute, 80 | dtMinuteLayout: dtMinute, 81 | dtHourLayoutTZ: dtHour, 82 | dtHourLayout: dtHour, 83 | dtDayLayout: dtDay, 84 | dtMonthLayout: dtMonth, 85 | dtYearLayout: dtYear, 86 | } 87 | -------------------------------------------------------------------------------- /fhirpath/system/quantity_test.go: -------------------------------------------------------------------------------- 1 | package system_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/verily-src/fhirpath-go/fhirpath/system" 7 | ) 8 | 9 | func TestParseQuantity_ErrorsOnInvalidString(t *testing.T) { 10 | testCases := []struct { 11 | name string 12 | number string 13 | unit string 14 | }{ 15 | { 16 | name: "non-number", 17 | number: "word", 18 | unit: "kg", 19 | }, 20 | { 21 | name: "empty strings", 22 | number: "", 23 | unit: "", 24 | }, 25 | } 26 | 27 | for _, tc := range testCases { 28 | t.Run(tc.name, func(t *testing.T) { 29 | if _, err := system.ParseQuantity(tc.number, tc.unit); err == nil { 30 | t.Fatalf("ParseQuantity(%s, %s) didn't raise error when expected", tc.number, tc.unit) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func TestParseQuantity_ReturnsQuantity(t *testing.T) { 37 | testCases := []struct { 38 | name string 39 | number string 40 | unit string 41 | }{ 42 | { 43 | name: "unit quantity", 44 | number: "100", 45 | unit: "lbs", 46 | }, 47 | { 48 | name: "time quantity", 49 | number: "3", 50 | unit: "minutes", 51 | }, 52 | } 53 | 54 | for _, tc := range testCases { 55 | t.Run(tc.name, func(t *testing.T) { 56 | if _, err := system.ParseQuantity(tc.number, tc.unit); err != nil { 57 | t.Fatalf("ParseQuantity(%s, %s) returned unexpected error: %v", tc.number, tc.unit, err) 58 | } 59 | }) 60 | } 61 | } 62 | 63 | func TestQuantity_Equal(t *testing.T) { 64 | onePound, _ := system.ParseQuantity("1", "lbs") 65 | oneLb, _ := system.ParseQuantity("1", "lbs") 66 | oneKg, _ := system.ParseQuantity("1", "kg") 67 | twoLbs, _ := system.ParseQuantity("2", "lbs") 68 | 69 | testCases := []struct { 70 | name string 71 | quantityOne system.Quantity 72 | quantityTwo system.Any 73 | shouldEqual bool 74 | wantOk bool 75 | }{ 76 | { 77 | name: "same quantity different objects", 78 | quantityOne: onePound, 79 | quantityTwo: oneLb, 80 | shouldEqual: true, 81 | wantOk: true, 82 | }, 83 | { 84 | name: "same number different unit", 85 | quantityOne: oneLb, 86 | quantityTwo: oneKg, 87 | wantOk: false, 88 | }, 89 | { 90 | name: "same unit different number", 91 | quantityOne: oneLb, 92 | quantityTwo: twoLbs, 93 | shouldEqual: false, 94 | wantOk: true, 95 | }, 96 | { 97 | name: "different type", 98 | quantityOne: oneLb, 99 | quantityTwo: system.String("1 lbs"), 100 | shouldEqual: false, 101 | wantOk: true, 102 | }, 103 | } 104 | 105 | for _, tc := range testCases { 106 | t.Run(tc.name, func(t *testing.T) { 107 | got, ok := tc.quantityOne.TryEqual(tc.quantityTwo) 108 | 109 | if ok != tc.wantOk { 110 | t.Fatalf("Quantity.Equal: ok got %v, want %v", ok, tc.wantOk) 111 | } 112 | if got != tc.shouldEqual { 113 | t.Errorf("Quantity.Equal returned unexpected equality: got %v, want %v", got, tc.shouldEqual) 114 | } 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /fhirpath/system/types.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | 8 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 9 | "github.com/shopspring/decimal" 10 | "github.com/verily-src/fhirpath-go/internal/fhir" 11 | "github.com/verily-src/fhirpath-go/internal/fhirconv" 12 | "github.com/verily-src/fhirpath-go/internal/protofields" 13 | ) 14 | 15 | var ErrCantBeCast = errors.New("value can't be cast to system type") 16 | 17 | // Any is the root abstraction for all FHIRPath system types. 18 | type Any interface { 19 | isSystemType() 20 | Name() string 21 | Less(input Any) (Boolean, error) 22 | } 23 | 24 | // Stub methods on each type to implement interface Any. 25 | func (s String) isSystemType() {} 26 | func (b Boolean) isSystemType() {} 27 | func (i Integer) isSystemType() {} 28 | func (d Decimal) isSystemType() {} 29 | func (d Date) isSystemType() {} 30 | func (t Time) isSystemType() {} 31 | func (d DateTime) isSystemType() {} 32 | func (q Quantity) isSystemType() {} 33 | 34 | // IsValid validates whether the input string represents 35 | // a valid system type name. 36 | func IsValid(typeName string) bool { 37 | switch typeName { 38 | case stringType, booleanType, integerType, decimalType, 39 | dateType, timeType, dateTimeType, quantityType, anyType: 40 | return true 41 | default: 42 | return false 43 | } 44 | } 45 | 46 | // IsPrimitive evaluates to check whether or not the input 47 | // is a primitive FHIR type. If so, returns true, otherwise 48 | // returns false 49 | func IsPrimitive(input any) bool { 50 | switch v := input.(type) { 51 | case *dtpb.Boolean, *dtpb.String, *dtpb.Uri, *dtpb.Url, *dtpb.Canonical, *dtpb.Code, *dtpb.Oid, *dtpb.Id, *dtpb.Uuid, *dtpb.Markdown, 52 | *dtpb.Base64Binary, *dtpb.Integer, *dtpb.UnsignedInt, *dtpb.PositiveInt, *dtpb.Decimal, *dtpb.Date, 53 | *dtpb.Time, *dtpb.DateTime, *dtpb.Instant, *dtpb.Quantity, Any: 54 | return true 55 | case fhir.Base: 56 | return protofields.IsCodeField(v) 57 | default: 58 | return false 59 | } 60 | } 61 | 62 | // From converts primitive FHIR types to System types. 63 | // Returns the input if already a System type, and an error 64 | // if the input is not convertible. 65 | func From(input any) (Any, error) { 66 | switch v := input.(type) { 67 | case *dtpb.Boolean: 68 | return Boolean(v.Value), nil 69 | case *dtpb.String: 70 | return String(v.Value), nil 71 | case *dtpb.Uri: 72 | return String(v.Value), nil 73 | case *dtpb.Url: 74 | return String(v.Value), nil 75 | case *dtpb.Code: 76 | return String(v.Value), nil 77 | case *dtpb.Oid: 78 | return String(v.Value), nil 79 | case *dtpb.Id: 80 | return String(v.Value), nil 81 | case *dtpb.Uuid: 82 | return String(v.Value), nil 83 | case *dtpb.Markdown: 84 | return String(v.Value), nil 85 | case *dtpb.Base64Binary: 86 | return String(base64.StdEncoding.EncodeToString(v.Value)), nil 87 | case *dtpb.Canonical: 88 | return String(v.Value), nil 89 | case *dtpb.Integer: 90 | return Integer(v.Value), nil 91 | case *dtpb.UnsignedInt: 92 | return Integer(v.Value), nil 93 | case *dtpb.PositiveInt: 94 | return Integer(v.Value), nil 95 | case *dtpb.Decimal: 96 | value, err := decimal.NewFromString(v.Value) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return Decimal(value), nil 101 | case *dtpb.Date: 102 | value, err := DateFromProto(v) 103 | if err != nil { 104 | return nil, err 105 | } 106 | return value, nil 107 | case *dtpb.Time: 108 | return TimeFromProto(v), nil 109 | case *dtpb.DateTime: 110 | value, err := DateTimeFromProto(v) 111 | if err != nil { 112 | return nil, err 113 | } 114 | return value, nil 115 | case *dtpb.Instant: 116 | value, err := ParseDateTime(fhirconv.InstantToString(v)) 117 | if err != nil { 118 | return nil, err 119 | } 120 | return value, nil 121 | case *dtpb.Quantity: 122 | value, err := decimal.NewFromString(v.Value.Value) 123 | if err != nil { 124 | return nil, err 125 | } 126 | unit := v.GetCode().GetValue() 127 | return Quantity{Decimal(value), unit}, nil 128 | case Any: 129 | return v, nil 130 | case fhir.Base: 131 | value, ok := protofields.StringValueFromCodeField(v) 132 | if !ok { 133 | return nil, fmt.Errorf("%w: complex type %T", ErrCantBeCast, input) 134 | } 135 | return String(value), nil 136 | default: 137 | return nil, fmt.Errorf("%w: %T", ErrCantBeCast, input) 138 | } 139 | } 140 | 141 | // Normalize casts the "from" type to the "to" type if implicit casting 142 | // is supported between the types. Otherwise, it returns the from input. 143 | func Normalize(from Any, to Any) Any { 144 | switch v := from.(type) { 145 | case Integer: 146 | if _, ok := to.(Decimal); ok { 147 | return Decimal(decimal.NewFromInt32(int32(v))) 148 | } 149 | if q, ok := to.(Quantity); ok { 150 | dec := Decimal(decimal.NewFromInt32(int32(v))) 151 | return Quantity{dec, q.unit} 152 | } 153 | case Decimal: 154 | if q, ok := to.(Quantity); ok { 155 | return Quantity{v, q.unit} 156 | } 157 | case Date: 158 | if _, ok := to.(DateTime); ok { 159 | newLayout := v.l + "T" 160 | return DateTime{v.date, newLayout} 161 | } 162 | default: 163 | return from 164 | } 165 | return from 166 | } 167 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/verily-src/fhirpath-go 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/antlr4-go/antlr/v4 v4.13.0 9 | github.com/google/fhir/go v0.7.4 10 | github.com/google/go-cmp v0.7.0 11 | github.com/google/uuid v1.6.0 12 | github.com/iancoleman/strcase v0.3.0 13 | github.com/mattbaird/jsonpatch v0.0.0-20230413205102-771768614e91 14 | github.com/shopspring/decimal v1.3.1 15 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 16 | google.golang.org/protobuf v1.36.3 17 | ) 18 | 19 | require ( 20 | bitbucket.org/creachadair/stringset v0.0.11 // indirect 21 | github.com/golang/protobuf v1.5.4 // indirect 22 | github.com/json-iterator/go v1.1.12 // indirect 23 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 24 | github.com/modern-go/reflect2 v1.0.2 // indirect 25 | github.com/pkg/errors v0.9.1 // indirect 26 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect 27 | github.com/stretchr/testify v1.10.0 // indirect 28 | golang.org/x/net v0.37.0 // indirect 29 | golang.org/x/sys v0.31.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /gotchas.md: -------------------------------------------------------------------------------- 1 | # FHIRPath Gotcha’s 2 | 3 | ## Empty collections are propagated 4 | 5 | * In FHIRPath, whenever an empty collection is encountered, rather than raising an error it gets propagated throughout the rest of the expression. This may make some issues difficult to catch. 6 | * Eg. given `Patient.name` -> `{}`, `Patient.name.family + ' MD'` -> `{}` 7 | 8 | ## Equality sometimes returns an empty collection { }, rather than false 9 | 10 | * If either collection is empty 11 | * If the **precision_ _**of Date, Time, or DateTime objects are mismatched 12 | * If the **dimension** of a Quantity unit is mismatched 13 | 14 | ## FHIR type specifiers are case-sensitive 15 | 16 | * **Primitive** types are denoted with lower case specifiers. 17 | * **Primitive** types that are written as upper case will be resolved as **System** types, not **FHIR** types. 18 | * Eg. `Patient.birthDate is date = **true**` but `Patient.birthDate is Date = **false**` 19 | * Case should match what’s listed [here](https://www.hl7.org/fhir/r4/datatypes.html) 20 | * System types always begin with an uppercase letter 21 | 22 | ## `As` Expression is _not_ a filter, expects singleton input 23 | 24 | * The as expression (`Observation.value as integer`) expects a singleton as input. For example, if you pass in a resource with multiple value fields, it will raise an error. 25 | * It doesn’t filter out things that don’t match the type. For this purpose, the `where()` function should be used -> `where(value is integer)` 26 | -------------------------------------------------------------------------------- /internal/bundle/bundle.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package bundle provides utilities for working with FHIR R4 Bundle proto 3 | definitions. This includes functionality for constructing/wrapping and 4 | unwrapping bundle/entry objects. 5 | */ 6 | package bundle 7 | 8 | import ( 9 | cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" 10 | bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" 11 | "github.com/verily-src/fhirpath-go/internal/bundleopt" 12 | "github.com/verily-src/fhirpath-go/internal/fhir" 13 | "github.com/verily-src/fhirpath-go/internal/resource" 14 | "github.com/verily-src/fhirpath-go/internal/slices" 15 | ) 16 | 17 | // New creates a new New by building it from the bundle options. 18 | // Use of this function directly is discouraged; prefer to use the various 19 | // *New functions instead for the explicit types. 20 | func New(bundleType cpb.BundleTypeCode_Value, options ...Option) *bcrpb.Bundle { 21 | bundle := &bcrpb.Bundle{ 22 | Type: &bcrpb.Bundle_TypeCode{ 23 | Value: bundleType, 24 | }, 25 | } 26 | return Extend(bundle, options...) 27 | } 28 | 29 | // Extend extends an existing bundle with the provided bundle options. 30 | // The options will be extended in-place; the return value is not necessary to be 31 | // looked at, but is available for convenience when used in fluent APIs. 32 | // 33 | // This decision was made to avoid cloning the bundle per-invocation, since in a 34 | // loop this would grow the cost involved with calling this function substantially. 35 | func Extend(bundle *bcrpb.Bundle, opts ...Option) *bcrpb.Bundle { 36 | bundleopt.Apply(bundle, opts...) 37 | return bundle 38 | } 39 | 40 | // NewTransaction is a helper function for building a transaction bundle. 41 | func NewTransaction(options ...Option) *bcrpb.Bundle { 42 | return New(cpb.BundleTypeCode_TRANSACTION, options...) 43 | } 44 | 45 | // NewCollection is a helper function for building a collection bundle. 46 | func NewCollection(options ...Option) *bcrpb.Bundle { 47 | return New(cpb.BundleTypeCode_COLLECTION, options...) 48 | } 49 | 50 | // NewBatch is a helper function for building a batch bundle. 51 | func NewBatch(options ...Option) *bcrpb.Bundle { 52 | return New(cpb.BundleTypeCode_BATCH, options...) 53 | } 54 | 55 | // NewHistory is a helper function for building a history bundle. 56 | func NewHistory(options ...Option) *bcrpb.Bundle { 57 | return New(cpb.BundleTypeCode_HISTORY, options...) 58 | } 59 | 60 | // NewSearchset is a helper function for building a searchset bundle. 61 | func NewSearchset(options ...Option) *bcrpb.Bundle { 62 | return New(cpb.BundleTypeCode_SEARCHSET, options...) 63 | } 64 | 65 | // NewTransactionResponse returns a new bundle with type TRANSACTION_RESPONSE. 66 | func NewTransactionResponse(entries ...*bcrpb.Bundle_Entry) *bcrpb.Bundle { 67 | return New(cpb.BundleTypeCode_TRANSACTION_RESPONSE, 68 | WithEntries(entries...)) 69 | } 70 | 71 | // Unwrap unwraps a bundle into a slice of resources. 72 | func Unwrap(bundle *bcrpb.Bundle) []fhir.Resource { 73 | return slices.Map(bundle.GetEntry(), UnwrapEntry) 74 | } 75 | 76 | // UnwrapMap unwraps a bundle into a map indexed by resource type. 77 | func UnwrapMap(bundle *bcrpb.Bundle) map[resource.Type][]fhir.Resource { 78 | resourceMap := map[resource.Type][]fhir.Resource{} 79 | resources := Unwrap(bundle) 80 | for _, res := range resources { 81 | resourceType := resource.TypeOf(res) 82 | resourceMap[resourceType] = append(resourceMap[resourceType], res) 83 | } 84 | return resourceMap 85 | } 86 | -------------------------------------------------------------------------------- /internal/bundle/bundle_entry_example_test.go: -------------------------------------------------------------------------------- 1 | package bundle_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/google/fhir/go/fhirversion" 9 | "github.com/google/fhir/go/jsonformat" 10 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 11 | r4pb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" 12 | ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" 13 | "github.com/mattbaird/jsonpatch" 14 | "github.com/verily-src/fhirpath-go/internal/bundle" 15 | "github.com/verily-src/fhirpath-go/internal/resource" 16 | "google.golang.org/protobuf/proto" 17 | ) 18 | 19 | var resIdentity, _ = resource.NewIdentity("Patient", "123", "") 20 | 21 | func ExamplePatchEntryFromBytes_stringPatch() { 22 | patch := `[{"op":"add","path":"/active","value":true}]` 23 | pEntry, err := bundle.PatchEntryFromBytes(resIdentity, []byte(patch)) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | fmt.Printf("PatchEntry: %+v", pEntry) 28 | } 29 | 30 | func ExamplePatchEntryFromBytes_mapPatch() { 31 | patch := []map[string]interface{}{ 32 | { 33 | "op": "replace", 34 | "path": "/active", 35 | "value": true, 36 | }, 37 | } 38 | payload, err := json.Marshal(patch) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | pEntry, err := bundle.PatchEntryFromBytes(resIdentity, payload) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | fmt.Printf("PatchEntry: %+v", pEntry) 47 | } 48 | 49 | // ExamplePatchEntryFromBytes_diffPatch creates a patch for the diff of two 50 | // given resources diff. 51 | func ExamplePatchEntryFromBytes_diffPatch() { 52 | res := &r4pb.ContainedResource{ 53 | OneofResource: &r4pb.ContainedResource_Patient{ 54 | Patient: &ppb.Patient{ 55 | Id: &dtpb.Id{ 56 | Value: "123", 57 | }, 58 | Active: &dtpb.Boolean{ 59 | Value: false, 60 | }, 61 | }, 62 | }, 63 | } 64 | newRes := proto.Clone(res).(*r4pb.ContainedResource) 65 | newRes.GetPatient().Active = &dtpb.Boolean{Value: true} 66 | 67 | m, err := jsonformat.NewMarshaller(false, "", "", fhirversion.R4) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | resB, err := m.Marshal(res) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | newResB, err := m.Marshal(newRes) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | 80 | patch, err := jsonpatch.CreatePatch(resB, newResB) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | pPayload, err := json.Marshal(patch) 85 | if err != nil { 86 | log.Fatal(err) 87 | } 88 | 89 | pEntry, err := bundle.PatchEntryFromBytes(resIdentity, pPayload) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | fmt.Printf("PatchEntry: %+v", pEntry) 94 | } 95 | -------------------------------------------------------------------------------- /internal/bundle/bundle_option.go: -------------------------------------------------------------------------------- 1 | package bundle 2 | 3 | import ( 4 | "time" 5 | 6 | cpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/codes_go_proto" 7 | bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" 8 | "github.com/verily-src/fhirpath-go/internal/bundleopt" 9 | "github.com/verily-src/fhirpath-go/internal/fhir" 10 | ) 11 | 12 | // Option is an option interface for constructing bundles 13 | // from raw data. 14 | type Option = bundleopt.BundleOption 15 | 16 | // entriesOpt is a bundle option for including bundle entries. 17 | type entriesOpt []*bcrpb.Bundle_Entry 18 | 19 | func (o entriesOpt) updateBundle(bundle *bcrpb.Bundle) { 20 | if len(o) > 0 { 21 | bundle.Entry = o 22 | if bundle.GetType().GetValue() == cpb.BundleTypeCode_SEARCHSET || bundle.GetType().GetValue() == cpb.BundleTypeCode_HISTORY { 23 | bundle.Total = fhir.UnsignedInt(uint32(len(o))) 24 | } 25 | } 26 | } 27 | 28 | // WithEntries adds bundle entries to a bundle. 29 | func WithEntries(entries ...*bcrpb.Bundle_Entry) Option { 30 | entriesopt := entriesOpt(entries) 31 | return bundleopt.Transform(entriesopt.updateBundle) 32 | } 33 | 34 | // timeOpt is a bundle option for including a timestamp. 35 | type timeOpt time.Time 36 | 37 | func (o timeOpt) updateBundle(bundle *bcrpb.Bundle) { 38 | bundle.Timestamp = fhir.Instant(time.Time(o)) 39 | } 40 | 41 | // WithTimestamp adds a given time to the bundle's timestamp. 42 | func WithTimestamp(t time.Time) Option { 43 | timeOpt := timeOpt(t) 44 | return bundleopt.Transform(timeOpt.updateBundle) 45 | } 46 | 47 | // WithTimestampNow adds the current time to the bundle's timestamp. 48 | func WithTimestampNow() Option { 49 | timeOpt := timeOpt(time.Now()) 50 | return bundleopt.Transform(timeOpt.updateBundle) 51 | } 52 | -------------------------------------------------------------------------------- /internal/bundleopt/bundleopt.go: -------------------------------------------------------------------------------- 1 | package bundleopt 2 | 3 | import ( 4 | bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" 5 | ) 6 | 7 | // BundleOption is an option interface for constructing bundles 8 | // from raw data. 9 | type BundleOption interface { 10 | updateBundle(bundle *bcrpb.Bundle) 11 | } 12 | 13 | type Transform func(b *bcrpb.Bundle) 14 | 15 | func (t Transform) updateBundle(entry *bcrpb.Bundle) { 16 | t(entry) 17 | } 18 | 19 | func Apply(bundle *bcrpb.Bundle, opts ...BundleOption) { 20 | for _, opt := range opts { 21 | opt.updateBundle(bundle) 22 | } 23 | } 24 | 25 | var _ BundleOption = (*Transform)(nil) 26 | -------------------------------------------------------------------------------- /internal/containedresource/contained_resource_example_test.go: -------------------------------------------------------------------------------- 1 | package containedresource_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" 8 | "github.com/verily-src/fhirpath-go/internal/containedresource" 9 | "github.com/verily-src/fhirpath-go/internal/fhir" 10 | "google.golang.org/protobuf/proto" 11 | ) 12 | 13 | func ExampleWrap() { 14 | patient := &patient_go_proto.Patient{ 15 | Id: fhir.ID("12345"), 16 | } 17 | 18 | cr := containedresource.Wrap(patient) 19 | 20 | fmt.Printf("Patient ID = %v", cr.GetPatient().GetId().GetValue()) 21 | // Output: Patient ID = 12345 22 | } 23 | 24 | func ExampleUnwrap() { 25 | patient := &patient_go_proto.Patient{ 26 | Id: fhir.ID("12345"), 27 | } 28 | cr := containedresource.Wrap(patient) 29 | 30 | unwrapped := containedresource.Unwrap(cr).(*patient_go_proto.Patient) 31 | 32 | if proto.Equal(patient, unwrapped) { 33 | fmt.Printf("Resources match!") 34 | } 35 | // Output: Resources match! 36 | } 37 | 38 | func ExampleTypeOf() { 39 | patient := &patient_go_proto.Patient{ 40 | Id: fhir.ID("12345"), 41 | } 42 | cr := containedresource.Wrap(patient) 43 | 44 | crType := containedresource.TypeOf(cr) 45 | 46 | fmt.Printf("Contained Resource type = %v", crType) 47 | // Output: Contained Resource type = Patient 48 | } 49 | 50 | func ExampleID() { 51 | patient := &patient_go_proto.Patient{ 52 | Id: fhir.ID("12345"), 53 | } 54 | cr := containedresource.Wrap(patient) 55 | 56 | id := containedresource.ID(cr) 57 | 58 | fmt.Printf("Contained Resource ID = %v", id) 59 | // Output: Contained Resource ID = 12345 60 | } 61 | 62 | func ExampleGenerateIfNoneExist() { 63 | patient := &patient_go_proto.Patient{ 64 | Id: fhir.ID("12345"), 65 | Identifier: []*dtpb.Identifier{ 66 | &dtpb.Identifier{ 67 | System: &dtpb.Uri{Value: "http://fake.com"}, 68 | Value: &dtpb.String{Value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8"}, 69 | }, 70 | }, 71 | } 72 | 73 | cr := containedresource.Wrap(patient) 74 | 75 | value, err := containedresource.GenerateIfNoneExist(cr, "http://fake.com", true) 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | headers := map[string]string{} 81 | 82 | if value != "" { 83 | headers["If-None-Exist"] = value 84 | } 85 | 86 | fmt.Printf("If-None-Exist: %v", headers["If-None-Exist"]) 87 | // Output: If-None-Exist: identifier=http%3A%2F%2Ffake.com%7C9efbf82d-7a58-4d14-bec1-63f8fda148a8 88 | } 89 | -------------------------------------------------------------------------------- /internal/element/canonical/canonical.go: -------------------------------------------------------------------------------- 1 | package canonical 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | 8 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 9 | "github.com/verily-src/fhirpath-go/internal/fhir" 10 | "github.com/verily-src/fhirpath-go/internal/resource" 11 | ) 12 | 13 | var ( 14 | // ErrNoCanonicalURL is an error returned when an API receives a canonical that 15 | // does not have a specified URL field. 16 | ErrNoCanonicalURL = errors.New("canonical does not contain url") 17 | 18 | // very basic regex for URL matching. 19 | // Fragment portion is from https://build.fhir.org/references.html#literal 20 | canonicalRegExp = regexp.MustCompile(`^(?P[^|#]+)(\|(?P[A-z0-9-_\.]+))?(#(?P[A-z0-9-_\.]{1,64}))?`) 21 | ) 22 | 23 | // canonicalConfig is an internal struct for holding canonical information that 24 | // can be updated from a CanonicalOpt. 25 | type canonicalConfig struct { 26 | fragment, version string 27 | } 28 | 29 | // Option is an option interface for constructing canonicals from raw 30 | // data. 31 | type Option interface { 32 | updateCanonical(data *canonicalConfig) 33 | } 34 | 35 | // WithFragment adds a "fragment" portion to Canonical references. 36 | func WithFragment(frag string) Option { 37 | return canonicalFragOpt(frag) 38 | } 39 | 40 | // canonicalFragOpt is a simple canonical option for fragment strings. 41 | type canonicalFragOpt string 42 | 43 | func (o canonicalFragOpt) updateCanonical(data *canonicalConfig) { 44 | data.fragment = string(o) 45 | } 46 | 47 | // WithVersion adds a "version" portion to Canonical references. 48 | func WithVersion(version string) Option { 49 | return canonicalVersionOpt(version) 50 | } 51 | 52 | // canonicalVersionOpt is a simple canonical option for version strings. 53 | type canonicalVersionOpt string 54 | 55 | func (o canonicalVersionOpt) updateCanonical(data *canonicalConfig) { 56 | data.version = string(o) 57 | } 58 | 59 | // New constructs an R4 FHIR New element from the specified 60 | // url string and canonical options. 61 | // 62 | // See: http://hl7.org/fhir/R4/datatypes.html#canonical 63 | func New(url string, opts ...Option) *dtpb.Canonical { 64 | data := &canonicalConfig{} 65 | for _, opt := range opts { 66 | opt.updateCanonical(data) 67 | } 68 | if data.version != "" { 69 | url = fmt.Sprintf("%v|%v", url, data.version) 70 | } 71 | if data.fragment != "" { 72 | url = fmt.Sprintf("%v#%v", url, data.fragment) 73 | } 74 | return &dtpb.Canonical{ 75 | Value: url, 76 | } 77 | } 78 | 79 | // FromResource creates an R4 FHIR FromResource element from a 80 | // resource that has a URL, such as a Questionnaire, Device, etc. 81 | // 82 | // If the input resource is nil, or if the resource does not have a URL 83 | // field assigned, this function will return the error `ErrNoCanonicalURL`. 84 | // 85 | // See: https://hl7.org/fhir/R4/datatypes.html#canonical and 86 | // https://hl7.org/fhir/R4/references.html#canonical 87 | func FromResource(resource fhir.CanonicalResource) (*dtpb.Canonical, error) { 88 | if resource == nil || resource.GetUrl() == nil { 89 | return nil, ErrNoCanonicalURL 90 | } 91 | return New(resource.GetUrl().GetValue()), nil 92 | } 93 | 94 | // FragmentFromResource creates an R4 FHIR Canonical element from a resource that 95 | // has a URL, such as a Questionnaire, Device, etc., and will mark it as a 96 | // fragment-reference. 97 | // 98 | // If the input resource is nil, or if the resource does not have a URL 99 | // field assigned, this function will return the error `ErrNoCanonicalURL`. 100 | // 101 | // See: https://hl7.org/fhir/R4/datatypes.html#canonical and 102 | // https://hl7.org/fhir/R4/references.html#canonical 103 | func FragmentFromResource(resource fhir.CanonicalResource) (*dtpb.Canonical, error) { 104 | if resource == nil || resource.GetUrl() == nil { 105 | return nil, ErrNoCanonicalURL 106 | } 107 | return New(resource.GetUrl().GetValue(), WithFragment(resource.GetId().GetValue())), nil 108 | } 109 | 110 | // VersionedFromResource creates an R4 FHIR Canonical element from a resource that 111 | // has a URL, such as a Questionnaire, Device, etc, along with a version string. 112 | // 113 | // If the input resource is nil, or if the resource does not have a URL 114 | // field assigned, this function will return the error `ErrNoCanonicalURL`. 115 | // 116 | // See: https://hl7.org/fhir/R4/datatypes.html#canonical and 117 | // https://hl7.org/fhir/R4/references.html#canonical 118 | func VersionedFromResource(resource fhir.CanonicalResource) (*dtpb.Canonical, error) { 119 | if resource == nil || resource.GetUrl() == nil { 120 | return nil, ErrNoCanonicalURL 121 | } 122 | url := resource.GetUrl() 123 | version := resource.GetVersion() 124 | if version == nil { 125 | return New(url.GetValue()), nil 126 | } 127 | return New(resource.GetUrl().GetValue(), WithVersion(version.GetValue())), nil 128 | } 129 | 130 | // IdentityFromReference returns an Identity object from a given canonical reference 131 | // Replaces: ph.ParseCanonical 132 | func IdentityFromReference(c *dtpb.Canonical) (*resource.CanonicalIdentity, error) { 133 | value := c.GetValue() 134 | match := canonicalRegExp.FindStringSubmatch(value) 135 | result := make(map[string]string) 136 | for i, name := range canonicalRegExp.SubexpNames() { 137 | if i != 0 && name != "" { 138 | result[name] = match[i] 139 | } 140 | } 141 | return resource.NewCanonicalIdentity(result["url"], result["version"], result["fragment"]) 142 | } 143 | 144 | // IdentityOf returns a canonicalIdentity representing the given canonical resource 145 | func IdentityOf(res fhir.CanonicalResource) (*resource.CanonicalIdentity, error) { 146 | if res == nil || res.GetUrl() == nil { 147 | return nil, ErrNoCanonicalURL 148 | } 149 | 150 | return resource.NewCanonicalIdentity(res.GetUrl().GetValue(), res.GetVersion().GetValue(), "") 151 | } 152 | -------------------------------------------------------------------------------- /internal/element/element.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package element defines subpackages focused on FHIR R4 elements. Each subpackage 3 | is named after the FHIR data type (e.g. identifier) and provides capabilities 4 | related to that type. 5 | */ 6 | package element 7 | -------------------------------------------------------------------------------- /internal/element/extension/extension_example_test.go: -------------------------------------------------------------------------------- 1 | package extension_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/task_go_proto" 8 | "github.com/verily-src/fhirpath-go/internal/element/extension" 9 | "github.com/verily-src/fhirpath-go/internal/fhir" 10 | ) 11 | 12 | func ExampleOverwrite() { 13 | const urlBase = "https://verily-src.github.io/vhp-hds-vvs-fhir-ig/StructureDefinitions" 14 | task := &task_go_proto.Task{ 15 | Extension: []*dtpb.Extension{ 16 | extension.New(fmt.Sprintf("%v/%v", urlBase, "my-int"), fhir.Integer(42)), 17 | }, 18 | } 19 | 20 | extension.Overwrite(task, 21 | extension.New(fmt.Sprintf("%v/%v", urlBase, "my-string"), fhir.String("hello world")), 22 | extension.New(fmt.Sprintf("%v/%v", urlBase, "my-bool"), fhir.Boolean(true)), 23 | ) 24 | fmt.Printf("%v extensions in Task!", len(task.GetExtension())) 25 | // Output: 2 extensions in Task! 26 | } 27 | 28 | func ExampleAppendInto() { 29 | const urlBase = "http://example.com/StructureDefinitions" 30 | task := &task_go_proto.Task{ 31 | Extension: []*dtpb.Extension{ 32 | extension.New(fmt.Sprintf("%v/%v", urlBase, "my-int"), fhir.Integer(42)), 33 | }, 34 | } 35 | 36 | extension.AppendInto(task, 37 | extension.New(fmt.Sprintf("%v/%v", urlBase, "my-string"), fhir.String("hello world")), 38 | extension.New(fmt.Sprintf("%v/%v", urlBase, "my-bool"), fhir.Boolean(true)), 39 | ) 40 | fmt.Printf("%v extensions in Task!", len(task.GetExtension())) 41 | // Output: 3 extensions in Task! 42 | } 43 | -------------------------------------------------------------------------------- /internal/element/identifier/docs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package identifier provides utilities for constructing and working with 3 | FHIR R4 Identifier elements. 4 | 5 | See: http://hl7.org/fhir/R4/datatypes.html#Identifier 6 | */ 7 | package identifier 8 | -------------------------------------------------------------------------------- /internal/element/identifier/identifier_example_test.go: -------------------------------------------------------------------------------- 1 | package identifier_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/verily-src/fhirpath-go/internal/element/identifier" 8 | ) 9 | 10 | func ExampleQueryIdentifier() { 11 | ident := &dtpb.Identifier{ 12 | System: &dtpb.Uri{Value: "http://fake.com"}, 13 | Value: &dtpb.String{Value: "b0459744-b74b-441a-aee4-9dd97c80c642"}, 14 | } 15 | 16 | search := identifier.QueryIdentifier(ident) 17 | fmt.Printf("identifier:exact=%s", search) 18 | // Output: identifier:exact=http://fake.com|b0459744-b74b-441a-aee4-9dd97c80c642 19 | } 20 | 21 | func ExampleQueryIdentifier_escape() { 22 | ident := &dtpb.Identifier{ 23 | System: &dtpb.Uri{Value: "http://fake.com"}, 24 | Value: &dtpb.String{Value: "foo,bar|baz"}, 25 | } 26 | 27 | search := identifier.QueryIdentifier(ident) 28 | fmt.Printf("identifier:exact=%s", search) 29 | // Output: identifier:exact=http://fake.com|foo\,bar\|baz 30 | } 31 | 32 | func ExampleQueryString() { 33 | search := identifier.QueryString("http://fake.com", "1234") 34 | fmt.Printf("identifier:exact=%s", search) 35 | // Output: identifier:exact=http://fake.com|1234 36 | } 37 | func ExampleQueryString_escape() { 38 | search := identifier.QueryString("http://fake.com", `$foo|bar\baz`) 39 | fmt.Printf("identifier:exact=%s", search) 40 | // Output: identifier:exact=http://fake.com|\$foo\|bar\\baz 41 | } 42 | 43 | func ExampleGenerateIfNoneExist() { 44 | id := &dtpb.Identifier{ 45 | System: &dtpb.Uri{Value: "http://fake.com"}, 46 | Value: &dtpb.String{Value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8"}, 47 | } 48 | 49 | header := identifier.GenerateIfNoneExist(id) 50 | fmt.Printf("If-None-Exist: %v", header) 51 | // Output: If-None-Exist: identifier=http%3A%2F%2Ffake.com%7C9efbf82d-7a58-4d14-bec1-63f8fda148a8 52 | } 53 | -------------------------------------------------------------------------------- /internal/element/identifier/opts.go: -------------------------------------------------------------------------------- 1 | package identifier 2 | 3 | import ( 4 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 5 | "github.com/verily-src/fhirpath-go/internal/fhir" 6 | ) 7 | 8 | // Option is an abstraction for options to construct or modify Identifier elements. 9 | type Option interface { 10 | update(*dtpb.Identifier) 11 | } 12 | 13 | // WithUse returns an Identifier Option that sets the Identifier.Use to the 14 | // specified use. 15 | func WithUse(use Use) Option { 16 | return withCallback(func(i *dtpb.Identifier) { 17 | i.Use = &dtpb.Identifier_UseCode{ 18 | Value: use, 19 | } 20 | }) 21 | } 22 | 23 | // WithExtensions return an Identifier Option that sets the Identifier.Extension 24 | // field to the specified extensions. 25 | func WithExtensions(ext ...*dtpb.Extension) Option { 26 | return withCallback(func(i *dtpb.Identifier) { 27 | i.Extension = ext 28 | }) 29 | } 30 | 31 | // IncludeExtensions return an Identifier Option that appends the specified 32 | // extensions to the Identifier.Extension field. 33 | func IncludeExtensions(ext ...*dtpb.Extension) Option { 34 | return withCallback(func(i *dtpb.Identifier) { 35 | i.Extension = append(i.Extension, ext...) 36 | }) 37 | } 38 | 39 | // WithType returns an Identifier Option that sets the Identifier.Type to the 40 | // specified type. 41 | func WithType(ty *dtpb.CodeableConcept) Option { 42 | return withCallback(func(i *dtpb.Identifier) { 43 | i.Type = ty 44 | }) 45 | } 46 | 47 | // WithSystem returns an Identifier Option that sets the Identifier.System to the 48 | // specified system. 49 | func WithSystem(system *dtpb.Uri) Option { 50 | return withCallback(func(i *dtpb.Identifier) { 51 | i.System = system 52 | }) 53 | } 54 | 55 | // WithSystemString returns an Identifier Option that sets the Identifier.System 56 | // to the specified system string. 57 | func WithSystemString(system string) Option { 58 | return WithSystem(fhir.URI(system)) 59 | } 60 | 61 | // WithValue returns an Identifier Option that sets the Identifier.Value to the 62 | // specified value. 63 | func WithValue(value string) Option { 64 | return withCallback(func(i *dtpb.Identifier) { 65 | i.Value = fhir.String(value) 66 | }) 67 | } 68 | 69 | // WithPeriod returns an Identifier Option that sets the Identifier.Period to the 70 | // specified period. 71 | func WithPeriod(period *dtpb.Period) Option { 72 | return withCallback(func(i *dtpb.Identifier) { 73 | i.Period = period 74 | }) 75 | } 76 | 77 | // WithAssigner returns an Identifier Option that sets the Identifier.Assigner to the 78 | // specified assigner reference. 79 | func WithAssigner(assigner *dtpb.Reference) Option { 80 | return withCallback(func(i *dtpb.Identifier) { 81 | i.Assigner = assigner 82 | }) 83 | } 84 | 85 | // WithID returns an Identifier Option that sets the Identifier.Id to the 86 | // specified ID. 87 | func WithID(id string) Option { 88 | return withCallback(func(i *dtpb.Identifier) { 89 | i.Id = fhir.String(id) 90 | }) 91 | } 92 | 93 | type callbackOpt struct { 94 | callback func(*dtpb.Identifier) 95 | } 96 | 97 | func (o callbackOpt) update(i *dtpb.Identifier) { 98 | o.callback(i) 99 | } 100 | 101 | func withCallback(callback func(*dtpb.Identifier)) Option { 102 | return callbackOpt{callback} 103 | } 104 | -------------------------------------------------------------------------------- /internal/element/meta/meta.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 5 | "github.com/verily-src/fhirpath-go/internal/fhir" 6 | "google.golang.org/protobuf/reflect/protoreflect" 7 | ) 8 | 9 | // Option is an option interface for modifying meta in place. 10 | type Option interface { 11 | updateMeta(meta *dtpb.Meta) 12 | } 13 | 14 | // Update updates meta in place with given opts. 15 | func Update(meta *dtpb.Meta, opts ...Option) *dtpb.Meta { 16 | for _, opt := range opts { 17 | opt.updateMeta(meta) 18 | } 19 | return meta 20 | } 21 | 22 | // WithTags replaces meta.tag. 23 | func WithTags(tags ...*dtpb.Coding) Option { 24 | return withCodingOpt(tags) 25 | } 26 | 27 | type withCodingOpt []*dtpb.Coding 28 | 29 | func (wco withCodingOpt) updateMeta(meta *dtpb.Meta) { 30 | meta.Tag = wco 31 | } 32 | 33 | // WithExtensions replaces meta.extension. 34 | func WithExtensions(exts ...*dtpb.Extension) Option { 35 | return withExtensionOpt(exts) 36 | } 37 | 38 | type withExtensionOpt []*dtpb.Extension 39 | 40 | func (weo withExtensionOpt) updateMeta(meta *dtpb.Meta) { 41 | meta.Extension = weo 42 | } 43 | 44 | // IncludeTags appends to meta.tag. 45 | func IncludeTags(tags ...*dtpb.Coding) Option { 46 | return includeCodingOpt(tags) 47 | } 48 | 49 | type includeCodingOpt []*dtpb.Coding 50 | 51 | func (ico includeCodingOpt) updateMeta(meta *dtpb.Meta) { 52 | meta.Tag = append(meta.Tag, ico...) 53 | } 54 | 55 | // WithProfiles replaces meta.profile. 56 | func WithProfiles(profiles ...*dtpb.Canonical) Option { 57 | return withCanonicalOpt(profiles) 58 | } 59 | 60 | type withCanonicalOpt []*dtpb.Canonical 61 | 62 | func (wco withCanonicalOpt) updateMeta(meta *dtpb.Meta) { 63 | meta.Profile = wco 64 | } 65 | 66 | // IncludeProfiles appends to meta.profile. 67 | func IncludeProfiles(profiles ...*dtpb.Canonical) Option { 68 | return includeCanonicalOpt(profiles) 69 | } 70 | 71 | type includeCanonicalOpt []*dtpb.Canonical 72 | 73 | func (ico includeCanonicalOpt) updateMeta(meta *dtpb.Meta) { 74 | meta.Profile = append(meta.Profile, ico...) 75 | } 76 | 77 | // ReplaceInResource replaces the resource meta field with the provided meta 78 | // object. 79 | func ReplaceInResource(resource fhir.Resource, meta *dtpb.Meta) { 80 | reflect := resource.ProtoReflect() 81 | metaField := getMetaField(reflect) 82 | 83 | reflect.Set(metaField, protoreflect.ValueOfMessage(meta.ProtoReflect())) 84 | } 85 | 86 | // EnsureInResource ensures that the resource meta field exists. 87 | func EnsureInResource(resource fhir.Resource) { 88 | if resource.GetMeta() == nil { 89 | ReplaceInResource(resource, &dtpb.Meta{}) 90 | } 91 | } 92 | 93 | func getMetaField(reflect protoreflect.Message) protoreflect.FieldDescriptor { 94 | return reflect.Descriptor().Fields().ByName("meta") 95 | } 96 | -------------------------------------------------------------------------------- /internal/element/meta/meta_example_test.go: -------------------------------------------------------------------------------- 1 | package meta_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/verily-src/fhirpath-go/internal/element/canonical" 8 | "github.com/verily-src/fhirpath-go/internal/element/meta" 9 | "github.com/verily-src/fhirpath-go/internal/fhir" 10 | ) 11 | 12 | func ExampleUpdate() { 13 | m := &dtpb.Meta{} 14 | 15 | meta.Update(m, 16 | meta.WithTags(fhir.Coding("urn:oid:verily/sample-tag-system", "sample-tag-value")), 17 | meta.WithProfiles(canonical.New("urn:oid:verily/sample-profile")), 18 | ) 19 | 20 | fmt.Printf("meta.profile: %q\n", m.Profile[0].Value) 21 | fmt.Printf("meta.tag: {%q, %q}", m.Tag[0].System.Value, m.Tag[0].Code.Value) 22 | // Output: 23 | // meta.profile: "urn:oid:verily/sample-profile" 24 | // meta.tag: {"urn:oid:verily/sample-tag-system", "sample-tag-value"} 25 | } 26 | -------------------------------------------------------------------------------- /internal/fhir/constraints.go: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import ( 4 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 5 | ) 6 | 7 | type primitiveDataType interface { 8 | *dtpb.Base64Binary | 9 | *dtpb.Boolean | 10 | *dtpb.Canonical | 11 | *dtpb.Code | 12 | *dtpb.Date | 13 | *dtpb.DateTime | 14 | *dtpb.Decimal | 15 | *dtpb.Id | 16 | *dtpb.Instant | 17 | *dtpb.Integer | 18 | *dtpb.Markdown | 19 | *dtpb.Oid | 20 | *dtpb.PositiveInt | 21 | *dtpb.String | 22 | *dtpb.Time | 23 | *dtpb.UnsignedInt | 24 | *dtpb.Uri | 25 | *dtpb.Url | 26 | *dtpb.Uuid 27 | } 28 | 29 | type complexDataType interface { 30 | *dtpb.Address | 31 | *dtpb.Age | 32 | *dtpb.Attachment | 33 | *dtpb.CodeableConcept | 34 | *dtpb.Coding | 35 | *dtpb.ContactPoint | 36 | *dtpb.Count | 37 | *dtpb.Distance | 38 | *dtpb.Duration | 39 | *dtpb.HumanName | 40 | *dtpb.Identifier | 41 | *dtpb.Money | 42 | *dtpb.MoneyQuantity | 43 | *dtpb.Period | 44 | *dtpb.Quantity | 45 | *dtpb.Range | 46 | *dtpb.Ratio | 47 | *dtpb.SampledData | 48 | *dtpb.Signature | 49 | *dtpb.SimpleQuantity | 50 | *dtpb.Timing 51 | } 52 | 53 | type metaDataType interface { 54 | *dtpb.ContactDetail | 55 | *dtpb.Contributor | 56 | *dtpb.DataRequirement | 57 | *dtpb.Expression | 58 | *dtpb.ParameterDefinition | 59 | *dtpb.RelatedArtifact | 60 | *dtpb.TriggerDefinition | 61 | *dtpb.UsageContext 62 | } 63 | 64 | type specialPurposeDataType interface { 65 | *dtpb.Dosage | 66 | *dtpb.ElementDefinition | 67 | *dtpb.Extension | 68 | *dtpb.MarketingStatus | 69 | *dtpb.Meta | 70 | *dtpb.Narrative | 71 | *dtpb.ProductShelfLife | 72 | *dtpb.Reference 73 | } 74 | 75 | // DataType is an constraint-definition of FHIR datatypes, which all support ID 76 | // and Extension fields, in addition to their base values. 77 | // 78 | // Note: "DataType" is also an "Element", so these interfaces are logically 79 | // equivalent -- and so this is represented as a constraint of valid datatypes. 80 | // 81 | // The R4 spec doesn't explicitly refer to "DataType" as a distinction from 82 | // "Element", but the R5 spec does, and its definition is compatible with R4. 83 | // This is retained here so that we can have a proper vernacular and mechanism 84 | // for referring to these types in generic ways through constraints. 85 | // 86 | // See https://www.hl7.org/fhir/r5/types.html#DataType for more details. 87 | type DataType interface { 88 | Element 89 | primitiveDataType | complexDataType | metaDataType | specialPurposeDataType 90 | } 91 | 92 | // PrimitiveType is a constraint-definition of FHIR datatypes, which all support ID 93 | // and Extension fields, in addition to their base values. 94 | // 95 | // Note: "DataType" is also an "Element", so these interfaces are logically 96 | // equivalent -- and so this is represented as a constraint of valid datatypes. 97 | // 98 | // The R4 spec doesn't explicitly refer to "PrimitiveType" as a distinction from 99 | // "Element", but the R5 spec does, and its definition is compatible with R4. 100 | // This is retained here so that we can have a proper vernacular and mechanism 101 | // for referring to these types in generic ways through constraints. 102 | // 103 | // See https://www.hl7.org/fhir/types.html#PrimitiveType for more details. 104 | type PrimitiveType interface { 105 | Element 106 | primitiveDataType 107 | } 108 | 109 | // BackboneType is a constraint-definition of FHIR backbone element, which all 110 | // support ID, Extension, and modifier-extension fields, in addition to their 111 | // base values. 112 | // 113 | // Note: "BackboneType" is also an "BackboneElement", so these interfaces are logically 114 | // equivalent -- and so this is represented as a constraint of valid datatypes. 115 | // 116 | // The R4 spec doesn't explicitly refer to "BackboneType" as a distinction from 117 | // "BackboneElement", but the R5 spec does, and its definition is compatible with R4. 118 | // This is retained here so that we can have a proper vernacular and mechanism 119 | // for referring to these types in generic ways through constraints. 120 | // 121 | // See https://www.hl7.org/fhir/r5/types.html#BackboneType for more details. 122 | type BackboneType interface { 123 | BackboneElement 124 | *dtpb.Timing | 125 | *dtpb.ElementDefinition | 126 | *dtpb.MarketingStatus | 127 | *dtpb.ProductShelfLife | 128 | *dtpb.Dosage 129 | } 130 | -------------------------------------------------------------------------------- /internal/fhir/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package fhir provides a library for working with R4 Google FHIR protos: 3 | https://github.com/google/fhir. 4 | 5 | This provides various quality-of-life utilities over the base FHIR definitions, 6 | such as: 7 | 8 | - Defining abstract base resources as their respective interfaces 9 | - Creation functions for Elements 10 | - Access utilities for ContainedResources and References 11 | - etc. 12 | */ 13 | package fhir 14 | -------------------------------------------------------------------------------- /internal/fhir/duration.go: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import ( 4 | "time" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/verily-src/fhirpath-go/internal/units" 8 | ) 9 | 10 | // DurationFromTime converts an R4 FHIR Time Element into an R4 FHIR Duration 11 | // value. 12 | // 13 | // If the underlying time has Second-based precision, the returned Duration will 14 | // also have seconds precision; otherwise this will fallback into nanosecond 15 | // precision. 16 | func DurationFromTime(t *dtpb.Time) *dtpb.Duration { 17 | // Constrain the time to 24 hours 18 | duration := time.Microsecond * time.Duration(t.GetValueUs()) % (time.Hour * 24) 19 | 20 | switch t.GetPrecision() { 21 | case dtpb.Time_SECOND: 22 | return Seconds(duration) 23 | case dtpb.Time_MILLISECOND: 24 | return Milliseconds(duration) 25 | case dtpb.Time_MICROSECOND: 26 | fallthrough 27 | default: 28 | return Microseconds(duration) 29 | } 30 | } 31 | 32 | // Duration creates a Duration proto with the provided value, computing the 33 | // largest whole-unit of time that can be used to represent the time. 34 | func Duration(d time.Duration) *dtpb.Duration { 35 | value := d.Nanoseconds() 36 | if value == 0 { 37 | return durationValue(float64(value), units.Days) 38 | } 39 | unitConversions := []struct { 40 | unit units.Time 41 | duration time.Duration 42 | }{ 43 | {units.Days, 24 * time.Hour}, 44 | {units.Hours, time.Hour}, 45 | {units.Minutes, time.Minute}, 46 | {units.Seconds, time.Second}, 47 | {units.Milliseconds, time.Millisecond}, 48 | {units.Microseconds, time.Microsecond}, 49 | {units.Nanoseconds, time.Nanosecond}, 50 | } 51 | for _, conversion := range unitConversions { 52 | if d >= conversion.duration && d == d.Round(conversion.duration) { 53 | numUnits := d / conversion.duration 54 | return durationValue(float64(numUnits), conversion.unit) 55 | } 56 | } 57 | return durationValue(float64(value), units.Nanoseconds) 58 | } 59 | 60 | // Nanoseconds creates a Duration proto with the specified time value, rounded 61 | // to nanosecond accuracy. 62 | func Nanoseconds(value time.Duration) *dtpb.Duration { 63 | return durationValue(float64(value.Nanoseconds()), units.Nanoseconds) 64 | } 65 | 66 | // Milliseconds creates a Duration proto with the specified time value, rounded 67 | // to millisecond accuracy. 68 | func Milliseconds(value time.Duration) *dtpb.Duration { 69 | millis := float64(value.Nanoseconds()) / float64(time.Millisecond.Nanoseconds()) 70 | return durationValue(millis, units.Milliseconds) 71 | } 72 | 73 | // Microseconds creates a Duration proto with the specified time value, rounded 74 | // to microsecond accuracy. 75 | func Microseconds(value time.Duration) *dtpb.Duration { 76 | micros := float64(value.Nanoseconds()) / float64(time.Microsecond.Nanoseconds()) 77 | return durationValue(micros, units.Microseconds) 78 | } 79 | 80 | // Seconds creates a Duration proto with the specified time value, rounded 81 | // to second accuracy. 82 | func Seconds(value time.Duration) *dtpb.Duration { 83 | return durationValue(value.Seconds(), units.Seconds) 84 | } 85 | 86 | // Minutes creates a Duration proto with the specified time value, rounded 87 | // to minute accuracy. 88 | func Minutes(value time.Duration) *dtpb.Duration { 89 | return durationValue(value.Minutes(), units.Minutes) 90 | } 91 | 92 | // Hours creates a Duration proto with the specified time value, rounded 93 | // to hour-accuracy. 94 | func Hours(value time.Duration) *dtpb.Duration { 95 | return durationValue(value.Hours(), units.Hours) 96 | } 97 | 98 | // Days creates a Duration proto with the specified time value, rounded 99 | // to day-accuracy. 100 | func Days(value time.Duration) *dtpb.Duration { 101 | return durationValue(value.Hours()/24, units.Days) 102 | } 103 | 104 | func durationValue(value float64, unit units.Time) *dtpb.Duration { 105 | return &dtpb.Duration{ 106 | Value: Decimal(value), 107 | Code: Code(unit.Symbol()), 108 | System: URI(unit.System()), 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /internal/fhir/duration_test.go: -------------------------------------------------------------------------------- 1 | package fhir_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/verily-src/fhirpath-go/internal/fhir" 10 | "github.com/verily-src/fhirpath-go/internal/units" 11 | "google.golang.org/protobuf/testing/protocmp" 12 | ) 13 | 14 | func newDuration(value int64, unit units.Time) *dtpb.Duration { 15 | return &dtpb.Duration{ 16 | Value: fhir.Decimal(float64(value)), 17 | Code: fhir.Code(unit.Symbol()), 18 | System: fhir.URI(unit.System()), 19 | } 20 | } 21 | 22 | func TestDurationFromTime(t *testing.T) { 23 | testCases := []struct { 24 | name string 25 | time string 26 | value int64 27 | multiplier int64 28 | unit units.Time 29 | }{ 30 | {"Seconds", "00:01:00", 1, int64(time.Minute) / int64(time.Second), units.Seconds}, 31 | {"Milliseconds", "00:01:00.000", 1, int64(time.Minute) / int64(time.Millisecond), units.Milliseconds}, 32 | {"Microseconds", "00:01:00.000000", 1, int64(time.Minute) / int64(time.Microsecond), units.Microseconds}, 33 | } 34 | for _, tc := range testCases { 35 | t.Run(tc.name, func(t *testing.T) { 36 | input := fhir.MustParseTime(tc.time) 37 | value := tc.value * tc.multiplier 38 | want := newDuration(value, tc.unit) 39 | 40 | got := fhir.DurationFromTime(input) 41 | 42 | if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { 43 | t.Errorf("DurationFromTime(%v): (-got +want):\n%v", tc.name, diff) 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func TestDuration(t *testing.T) { 50 | testCases := []struct { 51 | name string 52 | value int64 53 | multiplier int64 54 | unit units.Time 55 | }{ 56 | {"Zero", 0, 1, units.Days}, 57 | {"Nanoseconds", 805, 1, units.Nanoseconds}, 58 | {"Microseconds", 15, int64(time.Microsecond), units.Microseconds}, 59 | {"Milliseconds", 32, int64(time.Millisecond), units.Milliseconds}, 60 | {"Seconds", 42, int64(time.Second), units.Seconds}, 61 | {"Minutes", 1, int64(time.Minute), units.Minutes}, 62 | {"Hours", 12, int64(time.Hour), units.Hours}, 63 | {"Days", 3, 24 * int64(time.Hour), units.Days}, 64 | } 65 | for _, tc := range testCases { 66 | t.Run(tc.name, func(t *testing.T) { 67 | value := tc.value * tc.multiplier 68 | timeDuration := time.Duration(value) 69 | want := newDuration(tc.value, tc.unit) 70 | 71 | got := fhir.Duration(timeDuration) 72 | 73 | if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { 74 | t.Errorf("Duration(%v): (-got +want):\n%v", tc.name, diff) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | func TestNanoseconds(t *testing.T) { 81 | value := time.Duration(400) 82 | want := newDuration(int64(value), units.Nanoseconds) 83 | 84 | got := fhir.Nanoseconds(value) 85 | 86 | if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { 87 | t.Errorf("Nanoseconds: (-got +want):\n%v", diff) 88 | } 89 | } 90 | 91 | func TestMicroseconds(t *testing.T) { 92 | value := time.Duration(400) 93 | duration := time.Microsecond * value 94 | want := newDuration(int64(value), units.Microseconds) 95 | 96 | got := fhir.Microseconds(duration) 97 | 98 | if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { 99 | t.Errorf("Microseconds: (-got +want):\n%v", diff) 100 | } 101 | } 102 | 103 | func TestMilliseconds(t *testing.T) { 104 | value := 400 105 | duration := time.Millisecond * 400 106 | want := newDuration(int64(value), units.Milliseconds) 107 | 108 | got := fhir.Milliseconds(duration) 109 | 110 | if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { 111 | t.Errorf("Milliseconds: (-got +want):\n%v", diff) 112 | } 113 | } 114 | 115 | func TestSeconds(t *testing.T) { 116 | value := 400 117 | duration := time.Second * 400 118 | want := newDuration(int64(value), units.Seconds) 119 | 120 | got := fhir.Seconds(duration) 121 | 122 | if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { 123 | t.Errorf("Seconds: (-got +want):\n%v", diff) 124 | } 125 | } 126 | 127 | func TestMinutes(t *testing.T) { 128 | value := 400 129 | duration := time.Minute * 400 130 | want := newDuration(int64(value), units.Minutes) 131 | 132 | got := fhir.Minutes(duration) 133 | 134 | if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { 135 | t.Errorf("Minutes: (-got +want):\n%v", diff) 136 | } 137 | } 138 | 139 | func TestHours(t *testing.T) { 140 | value := 400 141 | duration := time.Hour * 400 142 | want := newDuration(int64(value), units.Hours) 143 | 144 | got := fhir.Hours(duration) 145 | 146 | if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { 147 | t.Errorf("Hours: (-got +want):\n%v", diff) 148 | } 149 | } 150 | 151 | func TestDays(t *testing.T) { 152 | value := 400 153 | duration := time.Hour * 24 * 400 154 | want := newDuration(int64(value), units.Days) 155 | 156 | got := fhir.Days(duration) 157 | 158 | if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { 159 | t.Errorf("Days: (-got +want):\n%v", diff) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /internal/fhir/elements_general_test.go: -------------------------------------------------------------------------------- 1 | package fhir_test 2 | 3 | import ( 4 | "testing" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/verily-src/fhirpath-go/internal/fhir" 9 | "google.golang.org/protobuf/testing/protocmp" 10 | ) 11 | 12 | func TestCodeableConcept_WithoutText(t *testing.T) { 13 | myCoding := fhir.Coding("my-system", "my-code") 14 | yourCoding := fhir.Coding("your-system", "your-code") 15 | testCases := []struct { 16 | name string 17 | text string 18 | codings []*dtpb.Coding 19 | want *dtpb.CodeableConcept 20 | }{ 21 | {"empty", "", nil, &dtpb.CodeableConcept{}}, 22 | {"full", "my-text", []*dtpb.Coding{myCoding, yourCoding}, 23 | &dtpb.CodeableConcept{ 24 | Coding: []*dtpb.Coding{myCoding, yourCoding}, 25 | Text: fhir.String("my-text"), 26 | }, 27 | }, 28 | {"without text", "", []*dtpb.Coding{myCoding}, 29 | &dtpb.CodeableConcept{ 30 | Coding: []*dtpb.Coding{myCoding}, 31 | // The key behavior is the absence of the Text element. 32 | }, 33 | }, 34 | } 35 | 36 | for _, tc := range testCases { 37 | t.Run(tc.name, func(t *testing.T) { 38 | sut := fhir.CodeableConcept(tc.text, tc.codings...) 39 | if diff := cmp.Diff(tc.want, sut, protocmp.Transform()); diff != "" { 40 | t.Errorf("CodeableConcept mismatch (-want, +got):\n%s", diff) 41 | } 42 | }) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /internal/fhir/elements_metadata.go: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 4 | 5 | // Metadata Types: 6 | // 7 | // The section below defines types from the "MetaDataTypes" heading in 8 | // http://hl7.org/fhir/R4/datatypes.html#open 9 | 10 | // ContactDetail creates an R4 FHIR ContactDetail element from a string value 11 | // and the specified contact-points. 12 | // 13 | // See: http://hl7.org/fhir/R4/metadatatypes.html#ContactDetail 14 | func ContactDetail(name string, telecom ...*dtpb.ContactPoint) *dtpb.ContactDetail { 15 | return &dtpb.ContactDetail{ 16 | Name: String(name), 17 | Telecom: telecom, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/fhir/elements_special.go: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 4 | 5 | // Special Types: 6 | // 7 | // The section below defines types from the "Special Types" heading in 8 | // http://hl7.org/fhir/R4/datatypes.html#open 9 | 10 | // Narrative creates a R4 FHIR Narrative element from a string value. 11 | // 12 | // See: http://hl7.org/fhir/R4/narrative.html 13 | func Narrative(value string) *dtpb.Narrative { 14 | return &dtpb.Narrative{ 15 | Div: XHTML(value), 16 | } 17 | } 18 | 19 | // XHTML creates an R4 FHIR XHTML element from a string value. 20 | // 21 | // See: http://hl7.org/fhir/R4/narrative.html#xhtml 22 | func XHTML(value string) *dtpb.Xhtml { 23 | return &dtpb.Xhtml{ 24 | Value: value, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/fhir/elements_special_test.go: -------------------------------------------------------------------------------- 1 | package fhir_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "github.com/verily-src/fhirpath-go/internal/fhir" 8 | ) 9 | 10 | func TestNarrative(t *testing.T) { 11 | want := "" 12 | 13 | sut := fhir.Narrative(want) 14 | 15 | if got := sut.GetDiv().GetValue(); !cmp.Equal(got, want) { 16 | t.Errorf("Narrative: got %v, want %v", got, want) 17 | } 18 | } 19 | 20 | func TestXHTML(t *testing.T) { 21 | want := "" 22 | 23 | sut := fhir.XHTML(want) 24 | 25 | if got := sut.GetValue(); !cmp.Equal(got, want) { 26 | t.Errorf("XHTML: got %v, want %v", got, want) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/fhir/encoding.go: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import "strings" 4 | 5 | // These characters have special meaning in FHIR Search queries 6 | const SearchSpecialChars = `\,$|` 7 | 8 | // Escape values intended for use as a parameter in a FHIR Search. 9 | // 10 | // These characters have special meaning in Search queries and must be backslash escaped: 11 | // 12 | // `\`, `|`, `,`, `$` 13 | // 14 | // This function assumes that URL-encoding is performed later. (Percent 15 | // encoding is automatically handled by the healthcare client library when 16 | // query params are passed as a map.) 17 | // 18 | // For example, `foo,bar` becomes `foo\,bar` 19 | func EscapeSearchParam(value string) string { 20 | out := value 21 | for _, crune := range SearchSpecialChars { 22 | c := string(crune) 23 | out = strings.ReplaceAll(out, c, `\`+c) 24 | } 25 | return out 26 | } 27 | -------------------------------------------------------------------------------- /internal/fhir/encoding_test.go: -------------------------------------------------------------------------------- 1 | package fhir_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/verily-src/fhirpath-go/internal/fhir" 8 | ) 9 | 10 | func TestEscapeSearchParam(t *testing.T) { 11 | 12 | testCases := []struct { 13 | input string 14 | want string 15 | }{ 16 | {``, ``}, 17 | {`\`, `\\`}, 18 | {`$`, `\$`}, 19 | {`,`, `\,`}, 20 | {`|`, `\|`}, 21 | {`C:\bin\go foo, bar, baz | omg $500!`, `C:\\bin\\go foo\, bar\, baz \| omg \$500!`}, 22 | } 23 | 24 | for i, tc := range testCases { 25 | t.Run(fmt.Sprintf("testCases[%d]", i), func(t *testing.T) { 26 | got := fhir.EscapeSearchParam(tc.input) 27 | 28 | if got != tc.want { 29 | t.Errorf("got %#v, want %#v", got, tc.want) 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/fhir/protofields.go: -------------------------------------------------------------------------------- 1 | package fhir 2 | 3 | import ( 4 | "github.com/verily-src/fhirpath-go/internal/protofields" 5 | ) 6 | 7 | // UnwrapValueX obtains the underlying Message for oneof ValueX 8 | // elements, which use a nested Choice field. Returns nil if the input message 9 | // doesn't have a Choice field, or if the Oneof descriptor is unpopulated. 10 | // See wrapped implementation for more information. 11 | func UnwrapValueX(element Base) Base { 12 | return protofields.UnwrapOneofField(element, "choice") 13 | } 14 | -------------------------------------------------------------------------------- /internal/fhirconv/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package fhirconv provides conversion utilities to Go-native types from FHIR R4 3 | Elements. 4 | */ 5 | package fhirconv 6 | -------------------------------------------------------------------------------- /internal/fhirconv/integer.go: -------------------------------------------------------------------------------- 1 | package fhirconv 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 8 | "github.com/verily-src/fhirpath-go/internal/narrow" 9 | "golang.org/x/exp/constraints" 10 | ) 11 | 12 | var ( 13 | // ErrIntegerTruncated is an error raised when an integer truncation occurs 14 | // during integer conversion 15 | ErrIntegerTruncated = errors.New("integer truncation") 16 | ) 17 | 18 | // integerType is a constraint for FHIR integer types. 19 | type integerType interface { 20 | *dtpb.Integer | *dtpb.UnsignedInt | *dtpb.PositiveInt 21 | } 22 | 23 | // ToInt8 converts a FHIR Integer type into a Go native int8. 24 | func ToInt8[From integerType](v From) (int8, error) { 25 | return ToInteger[int8](v) 26 | } 27 | 28 | // ToInt16 converts a FHIR Integer type into a Go native int16. 29 | func ToInt16[From integerType](v From) (int16, error) { 30 | return ToInteger[int16](v) 31 | } 32 | 33 | // ToInt32 converts a FHIR Integer type into a Go native int32. 34 | func ToInt32[From integerType](v From) (int32, error) { 35 | return ToInteger[int32](v) 36 | } 37 | 38 | // ToInt64 converts a FHIR Integer type into a Go native int64. 39 | func ToInt64[From integerType](v From) (int64, error) { 40 | return ToInteger[int64](v) 41 | } 42 | 43 | // ToInt converts a FHIR Integer type into a Go native int. 44 | func ToInt[From integerType](v From) (int, error) { 45 | return ToInteger[int](v) 46 | } 47 | 48 | // ToUint8 converts a FHIR Integer type into a Go native uint8. 49 | func ToUint8[From integerType](v From) (uint8, error) { 50 | return ToInteger[uint8](v) 51 | } 52 | 53 | // ToUint16 converts a FHIR Integer type into a Go native uint16. 54 | func ToUint16[From integerType](v From) (uint16, error) { 55 | return ToInteger[uint16](v) 56 | } 57 | 58 | // ToUint32 converts a FHIR Integer type into a Go native uint32. 59 | func ToUint32[From integerType](v From) (uint32, error) { 60 | return ToInteger[uint32](v) 61 | } 62 | 63 | // ToUint64 converts a FHIR Integer type into a Go native uint64. 64 | func ToUint64[From integerType](v From) (uint64, error) { 65 | return ToInteger[uint64](v) 66 | } 67 | 68 | // ToUint converts a FHIR Integer type into a Go native uint. 69 | func ToUint[From integerType](v From) (uint, error) { 70 | return ToInteger[uint](v) 71 | } 72 | 73 | // ToInteger converts a FHIR Integer type into a Go native integer type. 74 | // 75 | // If the value of the integer does not fit into the receiver integer type, 76 | // this function will return an ErrIntegerTruncated. 77 | func ToInteger[To constraints.Integer, From integerType](v From) (To, error) { 78 | var result To 79 | if val, ok := any(v).(interface{ GetValue() uint32 }); ok { 80 | if result, ok := narrow.ToInteger[To](uint64(val.GetValue())); ok { 81 | return result, nil 82 | } 83 | return 0, truncationError[To](val.GetValue()) 84 | } else if val, ok := any(v).(interface{ GetValue() int32 }); ok { 85 | 86 | if result, ok := narrow.ToInteger[To](int64(val.GetValue())); ok { 87 | return result, nil 88 | } 89 | return 0, truncationError[To](val.GetValue()) 90 | } 91 | // This cannot be reached because this function is constrained to only 92 | // take FHIR Elements that fit one of the above two types. 93 | return result, ErrIntegerTruncated 94 | } 95 | 96 | // MustConvertToInteger converts a FHIR Integer type into a Go native integer type. 97 | // 98 | // If the value stored in the integer type cannot fit into the receiver type, 99 | // this function will panic. 100 | func MustConvertToInteger[To constraints.Integer, From integerType](v From) To { 101 | result, err := ToInteger[To](v) 102 | if err != nil { 103 | panic(err) 104 | } 105 | return result 106 | } 107 | 108 | // truncationError forms an Error type for truncation errors. 109 | func truncationError[To constraints.Integer, From constraints.Integer](value From) error { 110 | var result To 111 | return fmt.Errorf("%w: type %T with value %v does not fit into receiver %T", ErrIntegerTruncated, value, value, result) 112 | } 113 | -------------------------------------------------------------------------------- /internal/fhirconv/string.go: -------------------------------------------------------------------------------- 1 | package fhirconv 2 | 3 | import ( 4 | b64 "encoding/base64" 5 | "fmt" 6 | "time" 7 | 8 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 9 | "github.com/verily-src/fhirpath-go/internal/fhir" 10 | ) 11 | 12 | // ToString converts a basic FHIR Primitive into a human-readable string 13 | // representation. 14 | func ToString[T fhir.PrimitiveType](val T) string { 15 | return toString(val) 16 | } 17 | 18 | // toString is the implementation of the ToString function. 19 | // 20 | // This is a separate function written in terms of an interface, rather than 21 | // being generic, to avoid unnecessary code-bloat -- since generics will generate 22 | // code for every instantiation with a different type. 23 | func toString(val fhir.Element) string { 24 | switch val := val.(type) { 25 | case interface{ GetValue() string }: 26 | // URI, URL, OID, UUID, Canonical, String, ID, Markdown, Code, Decimal 27 | return val.GetValue() 28 | case interface{ GetValue() bool }: 29 | // Boolean 30 | return fmt.Sprintf("%v", val.GetValue()) 31 | case interface{ GetValue() uint32 }: 32 | // PositiveInteger, UnsignedInt 33 | return fmt.Sprintf("%v", val.GetValue()) 34 | case interface{ GetValue() int32 }: 35 | // Integer 36 | return fmt.Sprintf("%v", val.GetValue()) 37 | case interface{ GetValue() []byte }: 38 | // Base64Binary 39 | return b64.StdEncoding.EncodeToString(val.GetValue()) 40 | case *dtpb.Instant: 41 | return InstantToString(val) 42 | case *dtpb.DateTime: 43 | return DateTimeToString(val) 44 | case *dtpb.Time: 45 | return TimeToString(val) 46 | case *dtpb.Date: 47 | return DateToString(val) 48 | } 49 | // This can't be reached; the above switch is exhaustive for all possible 50 | // inputs, which is restricted by the type constraint. 51 | return "" 52 | } 53 | 54 | // InstantToString converts the FHIR Instant element into its string reprsentation 55 | // as defined in http://hl7.org/fhir/R4/datatypes.html#instant. 56 | // 57 | // The level of precision in the output is equivalent to the precision defined 58 | // in the input Instant proto. 59 | func InstantToString(val *dtpb.Instant) string { 60 | if tm, err := InstantToTime(val); err == nil { 61 | switch val.GetPrecision() { 62 | case dtpb.Instant_SECOND: 63 | return tm.Format("2006-01-02T15:04:05-07:00") 64 | case dtpb.Instant_MILLISECOND: 65 | return tm.Format("2006-01-02T15:04:05.000-07:00") 66 | case dtpb.Instant_MICROSECOND: 67 | fallthrough 68 | default: 69 | return tm.Format("2006-01-02T15:04:05.000000-07:00") 70 | } 71 | } 72 | // Fall-back to a basic representation (this shouldn't happen unless timezone 73 | // information is garbage, which is a developer-driven issue). 74 | return fmt.Sprintf("Instant(%v)", val.GetValueUs()) 75 | } 76 | 77 | // DateTimeToString converts the FHIR DateTime element into its string reprsentation 78 | // as defined in http://hl7.org/fhir/R4/datatypes.html#datetime. 79 | // 80 | // The level of precision in the output is equivalent to the precision defined 81 | // in the input DateTime proto. 82 | func DateTimeToString(val *dtpb.DateTime) string { 83 | if tm, err := DateTimeToTime(val); err == nil { 84 | switch val.GetPrecision() { 85 | case dtpb.DateTime_YEAR: 86 | return tm.Format("2006") 87 | case dtpb.DateTime_MONTH: 88 | return tm.Format("2006-01") 89 | case dtpb.DateTime_DAY: 90 | return tm.Format("2006-01-02") 91 | case dtpb.DateTime_SECOND: 92 | return tm.Format("2006-01-02T15:04:05-07:00") 93 | case dtpb.DateTime_MILLISECOND: 94 | return tm.Format("2006-01-02T15:04:05.000-07:00") 95 | case dtpb.DateTime_MICROSECOND: 96 | fallthrough 97 | default: 98 | return tm.Format("2006-01-02T15:04:05.000000-07:00") 99 | } 100 | } 101 | 102 | // Fall-back to a basic representation (this shouldn't happen unless timezone 103 | // information is garbage, which is a developer-driven issue). 104 | return fmt.Sprintf("DateTime(%v)", val.GetValueUs()) 105 | } 106 | 107 | // DateToString converts the FHIR Date element into its string reprsentation 108 | // as defined in http://hl7.org/fhir/R4/datatypes.html#date. 109 | // 110 | // The level of precision in the output is equivalent to the precision defined 111 | // in the input Date proto. 112 | func DateToString(val *dtpb.Date) string { 113 | if tm, err := DateToTime(val); err == nil { 114 | switch val.GetPrecision() { 115 | case dtpb.Date_YEAR: 116 | return tm.Format("2006") 117 | case dtpb.Date_MONTH: 118 | return tm.Format("2006-01") 119 | case dtpb.Date_DAY: 120 | fallthrough 121 | default: 122 | return tm.Format("2006-01-02") 123 | } 124 | } 125 | 126 | // Fall-back to a basic representation (this shouldn't happen unless timezone 127 | // information is garbage, which is a developer-driven issue). 128 | return fmt.Sprintf("Date(%v)", val.GetValueUs()) 129 | } 130 | 131 | // TimeToString converts the FHIR Time element into its string reprsentation 132 | // as defined in http://hl7.org/fhir/R4/datatypes.html#time. 133 | // 134 | // The level of precision in the output is equivalent to the precision defined 135 | // in the input Time proto. 136 | func TimeToString(val *dtpb.Time) string { 137 | duration := TimeToDuration(val) 138 | 139 | hours := (duration / time.Hour) % (time.Hour * 24) 140 | duration %= time.Hour 141 | minutes := duration / time.Minute 142 | duration %= time.Minute 143 | seconds := duration / time.Second 144 | duration %= time.Second 145 | micros := duration / time.Microsecond 146 | 147 | switch val.GetPrecision() { 148 | case dtpb.Time_SECOND: 149 | return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) 150 | case dtpb.Time_MILLISECOND: 151 | millis := micros / 1_000 152 | return fmt.Sprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis) 153 | case dtpb.Time_MICROSECOND: 154 | fallthrough 155 | default: 156 | return fmt.Sprintf("%02d:%02d:%02d.%06d", hours, minutes, seconds, micros) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /internal/fhirconv/time.go: -------------------------------------------------------------------------------- 1 | package fhirconv 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 9 | "github.com/verily-src/fhirpath-go/internal/units" 10 | ) 11 | 12 | // DateTimeToTime converts a FHIR DateTime element into a Go time.Time value. 13 | // 14 | // This function will only error if the TimeZone field is invalid. 15 | // 16 | // Note: the error that can be returned from this function is unlikely to actually 17 | // occur in practice. FHIR Timezones are always required to be specified in the 18 | // form `+zz:zz` or `-zz:zz` -- and the JSON conversion translates this into 19 | // exactly this form, an empty string, or "UTC" in some cases. These are all 20 | // valid locations and should never fail as a result. 21 | func DateTimeToTime(dt *dtpb.DateTime) (time.Time, error) { 22 | tz, err := parseLocation(dt.GetTimezone()) 23 | if err != nil { 24 | return time.Time{}, fmt.Errorf("fhirconv.TimeFromDateTime: %w", err) 25 | } 26 | return time.UnixMicro(dt.GetValueUs()).In(tz), nil 27 | } 28 | 29 | // InstantToTime converts a FHIR Instant element into a Go time.Time value. 30 | // 31 | // This function will only error if the TimeZone field is invalid. 32 | // 33 | // Note: the error that can be returned from this function is unlikely to actually 34 | // occur in practice. FHIR Timezones are always required to be specified in the 35 | // form `+zz:zz` or `-zz:zz` -- and the JSON conversion translates this into 36 | // exactly this form, an empty string, or "UTC" in some cases. These are all 37 | // valid locations and should never fail as a result. 38 | func InstantToTime(dt *dtpb.Instant) (time.Time, error) { 39 | tz, err := parseLocation(dt.GetTimezone()) 40 | if err != nil { 41 | return time.Time{}, fmt.Errorf("fhirconv.TimeFromInstant: %w", err) 42 | } 43 | return time.UnixMicro(dt.GetValueUs()).In(tz), nil 44 | } 45 | 46 | // DateToTime converts a FHIR Date element into a Go time.Time value. 47 | // 48 | // This function will only error if the TimeZone field is invalid. 49 | // 50 | // Note: the error that can be returned from this function is unlikely to actually 51 | // occur in practice. FHIR Timezones are always required to be specified in the 52 | // form `+zz:zz` or `-zz:zz` -- and the JSON conversion translates this into 53 | // exactly this form, an empty string, or "UTC" in some cases. These are all 54 | // valid locations and should never fail as a result. 55 | func DateToTime(dt *dtpb.Date) (time.Time, error) { 56 | tz, err := parseLocation(dt.GetTimezone()) 57 | if err != nil { 58 | return time.Time{}, fmt.Errorf("fhirconv.Date: %w", err) 59 | } 60 | return time.UnixMicro(dt.GetValueUs()).In(tz), nil 61 | } 62 | 63 | // TimeToDuration converts a FHIR Time element into a Go time.Duration value. 64 | // 65 | // Despite the name `Time` for the FHIR Element, the time is not actually 66 | // associated to any real date -- and thus does not correspond to a distinct 67 | // chronological point, and thus cannot be converted logically into a `time.Time` 68 | // object. 69 | func TimeToDuration(dt *dtpb.Time) time.Duration { 70 | return time.Microsecond * time.Duration(dt.GetValueUs()) 71 | } 72 | 73 | // parseLocation attempts to parse the timezone location from the zone string. 74 | // 75 | // Timezones may be specified in one of 3 formats: 76 | // - Z 77 | // - +zz:zz or -zz:zz 78 | // - UTC (or some name) 79 | // 80 | // Additionally, this function supports empty strings being translated into 81 | // UTC. 82 | func parseLocation(zone string) (*time.Location, error) { 83 | if zone == "" { 84 | return time.UTC, nil 85 | } 86 | if tm, err := time.Parse("MST", zone); err == nil { 87 | return tm.Location(), nil 88 | } 89 | if tm, err := time.Parse("Z07:00", zone); err == nil { 90 | return tm.Location(), nil 91 | } 92 | if zone == "Local" { 93 | return time.Local, nil 94 | } 95 | return nil, fmt.Errorf("unable to parse time-zone from '%v'", zone) 96 | } 97 | 98 | // DurationToDuration converts a FHIR Duration element into a Go native 99 | // time.Duration object. 100 | // 101 | // This function may return an error in the following conditions: 102 | // - The underlying Decimal value is not able to be parsed into a float64 103 | // - The unit is not a valid time unit 104 | func DurationToDuration(d *dtpb.Duration) (time.Duration, error) { 105 | value := d.GetValue().GetValue() 106 | decimal, err := strconv.ParseFloat(value, 64) 107 | if err != nil { 108 | return durationToDurationError("bad decimal value '%v': %v", value, err) 109 | } 110 | code := d.GetCode().GetValue() 111 | 112 | unit, err := units.TimeFromSymbol(code) 113 | if err != nil { 114 | return durationToDurationError("invalid unit symbol '%v'", code) 115 | } 116 | 117 | symbol := unit.Symbol() 118 | 119 | // Special handling is necessary as days are not supported by 120 | // time.ParseDuration, as well as the minutes unit being m, not min 121 | switch unit { 122 | case units.Minutes: 123 | symbol = "m" 124 | case units.Days: 125 | decimal *= 24 126 | symbol = units.Hours.Symbol() 127 | } 128 | 129 | duration, err := time.ParseDuration(fmt.Sprintf("%v%s", decimal, symbol)) 130 | if err != nil { 131 | // This branch should not be possible to be reached. If we have reached this, 132 | // something really bad has happened -- because we form the format string 133 | // manually above. 134 | return durationToDurationError("%v", err) 135 | } 136 | return duration, nil 137 | } 138 | 139 | func durationToDurationError(format string, args ...any) (time.Duration, error) { 140 | message := fmt.Sprintf(format, args...) 141 | return time.Duration(0), fmt.Errorf("fhirconv.DurationToDuration: %v", message) 142 | } 143 | -------------------------------------------------------------------------------- /internal/fhirtest/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package fhirtest provides resource test dummies and useful utilities to enable 3 | better testing of the R4 FHIR Protos. 4 | 5 | This provides: 6 | - Pseudo-randomized FHIR resource identity generation 7 | - Construction utilities for forming new resources at runtime 8 | - Utilities for emulating Meta updates to FHIR resources 9 | - Resources are organized by their higher-level interface abstractions (e.g. 10 | organized by Resource, DomainResource, etc), and are keyed by resource-name. 11 | */ 12 | package fhirtest 13 | -------------------------------------------------------------------------------- /internal/fhirtest/elements.go: -------------------------------------------------------------------------------- 1 | package fhirtest 2 | 3 | import ( 4 | "github.com/verily-src/fhirpath-go/internal/fhir" 5 | "github.com/verily-src/fhirpath-go/internal/protofields" 6 | ) 7 | 8 | var ( 9 | // Elements is a map of all element-names to an instance of that element type. 10 | // 11 | // The elements in this map are not guaranteed to contain any specific value; 12 | // it is only guaranteed to contain a non-nil instance of a concrete element 13 | // of the associated name. 14 | Elements map[string]fhir.Element 15 | 16 | // BackboneElements is a map of all backbone element-names to an instance of 17 | // that element type. 18 | // 19 | // The elements in this map are not guaranteed to contain any specific value; 20 | // it is only guaranteed to contain a non-nil instance of a concrete backbone 21 | // element of the associated name. 22 | BackboneElements map[string]fhir.BackboneElement 23 | ) 24 | 25 | func init() { 26 | Elements = map[string]fhir.Element{} 27 | BackboneElements = map[string]fhir.BackboneElement{} 28 | 29 | for _, msg := range protofields.Elements { 30 | element, ok := msg.New().(fhir.Element) 31 | 32 | // The proto definition of the XHtml type is missing `GetValue()`, and thus 33 | // fails this check. This is added to avoid errors here that are otherwise 34 | // correct for all other cases. 35 | if !ok { 36 | continue 37 | } 38 | 39 | name := protofields.DescriptorName(element) 40 | 41 | Elements[name] = element 42 | if val, ok := any(element).(fhir.BackboneElement); ok { 43 | BackboneElements[name] = val 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/fhirtest/identity.go: -------------------------------------------------------------------------------- 1 | package fhirtest 2 | 3 | import ( 4 | "testing" 5 | 6 | bcrpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/bundle_and_contained_resource_go_proto" 7 | "github.com/verily-src/fhirpath-go/internal/bundle" 8 | "github.com/verily-src/fhirpath-go/internal/fhir" 9 | "github.com/verily-src/fhirpath-go/internal/resource" 10 | ) 11 | 12 | // NewIdentity creates a new resource identity. If an error occurs, it logs the error and fails the test. 13 | func NewIdentity(t *testing.T, resourceType, id, versionID string) *resource.Identity { 14 | t.Helper() 15 | identity, err := resource.NewIdentity(resourceType, id, versionID) 16 | if err != nil { 17 | t.Fatalf("NewIdentity: %v", err) 18 | } 19 | return identity 20 | } 21 | 22 | // NewIdentityFromURL creates a resource identity from the provided URL. If an error occurs, it logs the error and fails 23 | // the test. 24 | func NewIdentityFromURL(t *testing.T, url string) *resource.Identity { 25 | t.Helper() 26 | identity, err := resource.NewIdentityFromURL(url) 27 | if err != nil { 28 | t.Fatalf("url %s can't be converted to an identity", url) 29 | } 30 | return identity 31 | } 32 | 33 | // NewIdentityOf creates an identity from the provided resource. If an error occurs, it logs the error and fails the 34 | // test. 35 | func NewIdentityOf(t *testing.T, res fhir.Resource) *resource.Identity { 36 | t.Helper() 37 | identity, ok := resource.IdentityOf(res) 38 | if !ok { 39 | t.Fatalf("resource %T does not have identity", res) 40 | } 41 | return identity 42 | } 43 | 44 | // NewIdentityFromBundle returns the identity of the i-th resource in the given bundle. If the bundle does not contain 45 | // the resource, or if the resource does not contain an ID, the test fails and logs the error. 46 | func NewIdentityFromBundle(t *testing.T, bndl *bcrpb.Bundle, i int) *resource.Identity { 47 | t.Helper() 48 | entries := bndl.GetEntry() 49 | if i < 0 || i >= len(entries) { 50 | t.Fatalf("failed to get %d-th entry, bundle has %d entries", i, len(entries)) 51 | } 52 | return NewIdentityOf(t, bundle.UnwrapEntry(bndl.GetEntry()[i])) 53 | } 54 | -------------------------------------------------------------------------------- /internal/fhirtest/meta.go: -------------------------------------------------------------------------------- 1 | package fhirtest 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/verily-src/fhirpath-go/internal/element/meta" 8 | "github.com/verily-src/fhirpath-go/internal/fhir" 9 | "google.golang.org/protobuf/reflect/protoreflect" 10 | ) 11 | 12 | // TouchMeta touches the meta to generate a new version ID and use now as the 13 | // time. In most cases, this is likely what is required for tests -- since code 14 | // should seldomly rely on what the version-id or update times discretely are, and 15 | // this helps to ensure proper uniqueness just as the real FHIRStore would. 16 | func TouchMeta(resource fhir.Resource) { 17 | UpdateMeta(resource, randomVersionID(), time.Now()) 18 | } 19 | 20 | // UpdateID will update the resource's ID to the specified resourceID string. 21 | func UpdateID(resource fhir.Resource, resourceID string) { 22 | reflect := resource.ProtoReflect() 23 | field := reflect.Descriptor().Fields().ByName("id") 24 | 25 | id := &datatypes_go_proto.Id{ 26 | Value: resourceID, 27 | } 28 | reflect.Set(field, protoreflect.ValueOfMessage(id.ProtoReflect())) 29 | } 30 | 31 | // UpdateMeta updates the meta contents of the fhir resource to use the new 32 | // version-ID and update-time. 33 | func UpdateMeta(resource fhir.Resource, versionID string, updateTime time.Time) { 34 | reflect := resource.ProtoReflect() 35 | metaField := getMetaField(reflect) 36 | time := fhir.Instant(updateTime) 37 | version := fhir.ID(versionID) 38 | 39 | if resource.GetMeta() == nil { 40 | meta := &datatypes_go_proto.Meta{ 41 | LastUpdated: time, 42 | VersionId: version, 43 | } 44 | reflect.Set(metaField, protoreflect.ValueOfMessage(meta.ProtoReflect())) 45 | } 46 | 47 | message := reflect.Get(metaField).Message() 48 | descriptor := message.Descriptor() 49 | fields := descriptor.Fields() 50 | updateField := fields.ByName("last_updated") 51 | versionField := fields.ByName("version_id") 52 | 53 | message.Set(updateField, protoreflect.ValueOfMessage(time.ProtoReflect())) 54 | message.Set(versionField, protoreflect.ValueOfMessage(version.ProtoReflect())) 55 | } 56 | 57 | // NOTE: This method is deprecated and should use the production one in 58 | // "github.com/verily-src/fhirpath-go/internal/element/meta" 59 | func ReplaceMeta(resource fhir.Resource, m *datatypes_go_proto.Meta) { 60 | meta.ReplaceInResource(resource, m) 61 | } 62 | 63 | func getMetaField(reflect protoreflect.Message) protoreflect.FieldDescriptor { 64 | return reflect.Descriptor().Fields().ByName("meta") 65 | } 66 | -------------------------------------------------------------------------------- /internal/fhirtest/random.go: -------------------------------------------------------------------------------- 1 | package fhirtest 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 8 | "github.com/verily-src/fhirpath-go/internal/fhir" 9 | "github.com/verily-src/fhirpath-go/internal/stablerand" 10 | ) 11 | 12 | // stableRandomID generates a random ID value that will be stable across 13 | // multiple test executions. 14 | func stableRandomID() *datatypes_go_proto.Id { 15 | return &datatypes_go_proto.Id{ 16 | Value: randomID(), 17 | } 18 | } 19 | 20 | // stableRandomVersionID generates a random version-ID value that will be 21 | // stable across multiple test executions. 22 | func stableRandomVersionID() *datatypes_go_proto.Id { 23 | return &datatypes_go_proto.Id{ 24 | Value: randomVersionID(), 25 | } 26 | } 27 | 28 | func stableRandomInstant() *datatypes_go_proto.Instant { 29 | return fhir.Instant(stableRandomTime()) 30 | } 31 | 32 | func StableRandomMeta() *datatypes_go_proto.Meta { 33 | return &datatypes_go_proto.Meta{ 34 | LastUpdated: stableRandomInstant(), 35 | VersionId: stableRandomVersionID(), 36 | } 37 | } 38 | 39 | func randomVersionID() string { 40 | const versionIDLength = 26 41 | return stablerand.AlnumString(versionIDLength) 42 | } 43 | 44 | // This is a different implementation than fhir.RandomID() since this test 45 | // library manually sets a random seed. 46 | func randomID() string { 47 | uuidBase := stablerand.HexString(32) 48 | return fmt.Sprintf( 49 | "%v-%v-%v-%v-%v", 50 | uuidBase[0:8], 51 | uuidBase[8:12], 52 | uuidBase[12:16], 53 | uuidBase[16:20], 54 | uuidBase[20:], 55 | ) 56 | } 57 | 58 | func stableRandomTime() time.Time { 59 | const ( 60 | // Timestamp for 2020-01-01 T12:00:00 61 | baseTime int64 = 1577898000 62 | 63 | // Variation of up to 1 year 64 | timeVariation = time.Hour * 24 * 365 65 | ) 66 | 67 | base := time.Unix(baseTime, 0) 68 | return stablerand.Time(base, timeVariation) 69 | } 70 | -------------------------------------------------------------------------------- /internal/fhirtest/reference.go: -------------------------------------------------------------------------------- 1 | package fhirtest 2 | 3 | import ( 4 | "testing" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/verily-src/fhirpath-go/internal/element/reference" 8 | "github.com/verily-src/fhirpath-go/internal/resource" 9 | ) 10 | 11 | // NewReferenceTyped constructs a new strong typed un-versioned FHIR reference. If an error occurs, this function will 12 | // log it and fail the test. 13 | func NewReferenceTyped(t *testing.T, resourceType resource.Type, resourceId string) *dtpb.Reference { 14 | ref, err := reference.Typed(resourceType, resourceId) 15 | if err != nil { 16 | t.Fatalf("NewReferenceTyped: %v", err) 17 | } 18 | return ref 19 | } 20 | 21 | // NewReferenceVersionedTyped constructs a new strong typed versioned FHIR reference. If an error occurs, this function 22 | // will log it and fail the test. 23 | func NewReferenceVersionedTyped(t *testing.T, resourceType resource.Type, resourceId string, versionId string) *dtpb.Reference { 24 | ref, err := reference.Typed(resourceType, resourceId+"/_history/"+versionId) 25 | if err != nil { 26 | t.Fatalf("NewReferenceVersionedTyped: %v", err) 27 | } 28 | return ref 29 | } 30 | -------------------------------------------------------------------------------- /internal/fhirtest/resources_example_test.go: -------------------------------------------------------------------------------- 1 | package fhirtest_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 8 | ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" 9 | "github.com/verily-src/fhirpath-go/internal/fhir" 10 | "github.com/verily-src/fhirpath-go/internal/fhirtest" 11 | ) 12 | 13 | func ExampleWithResourceModification() { 14 | t := &testing.T{} 15 | 16 | patient := fhirtest.NewResource(t, "Patient", fhirtest.WithResourceModification(func(p *ppb.Patient) { 17 | p.Name = []*dtpb.HumanName{{Family: fhir.String("Ursa")}} 18 | })).(*ppb.Patient) 19 | 20 | fmt.Printf("patient.Name[0].Family = %v", patient.Name[0].Family) 21 | // Output: patient.Name[0].Family = value:"Ursa" 22 | } 23 | 24 | func ExampleNewResourceFromBase() { 25 | t := &testing.T{} 26 | original := &ppb.Patient{ 27 | Id: fhir.ID("uuid-a"), 28 | Name: []*dtpb.HumanName{{Family: fhir.String("Ursa")}}, 29 | } 30 | 31 | // Apply options on original. 32 | modified := fhirtest.NewResourceFromBase(t, original, 33 | fhirtest.WithResourceModification(func(p *ppb.Patient) { 34 | p.Name[0].Family = fhir.String("Major") 35 | p.Name[0].Given = fhir.Strings("Aseem") 36 | }), 37 | fhirtest.WithProtoField("id", fhir.ID("uuid-b")), 38 | ).(*ppb.Patient) 39 | 40 | fmt.Printf("ID = '%v', Family = '%v', Given = '%v'", 41 | modified.Id.Value, 42 | modified.Name[0].Family.Value, 43 | modified.Name[0].Given[0].Value, 44 | ) 45 | // Output: ID = 'uuid-b', Family = 'Major', Given = 'Aseem' 46 | } 47 | -------------------------------------------------------------------------------- /internal/protofields/descriptor.go: -------------------------------------------------------------------------------- 1 | package protofields 2 | 3 | import "google.golang.org/protobuf/proto" 4 | 5 | // DescriptorName gets the type name of a proto Message. If value is nil, this 6 | // returns an empty string. 7 | func DescriptorName(value proto.Message) string { 8 | if value == nil { 9 | return "" 10 | } 11 | return string(value.ProtoReflect().Descriptor().Name()) 12 | } 13 | -------------------------------------------------------------------------------- /internal/protofields/fields_test.go: -------------------------------------------------------------------------------- 1 | package protofields_test 2 | 3 | import ( 4 | "testing" 5 | 6 | opb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/observation_go_proto" 7 | ppb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/verily-src/fhirpath-go/internal/fhir" 10 | "github.com/verily-src/fhirpath-go/internal/protofields" 11 | "google.golang.org/protobuf/proto" 12 | "google.golang.org/protobuf/testing/protocmp" 13 | ) 14 | 15 | func TestUnwrapChoiceField_GetsUnderlyingMessage(t *testing.T) { 16 | dateTime := fhir.DateTimeNow() 17 | 18 | testCases := []struct { 19 | name string 20 | input proto.Message 21 | want proto.Message 22 | }{ 23 | { 24 | name: "gets boolean of Patient deceased field", 25 | input: &ppb.Patient_DeceasedX{ 26 | Choice: &ppb.Patient_DeceasedX_Boolean{ 27 | Boolean: fhir.Boolean(true), 28 | }, 29 | }, 30 | want: fhir.Boolean(true), 31 | }, 32 | { 33 | name: "gets date of Patient deceased field", 34 | input: &ppb.Patient_DeceasedX{ 35 | Choice: &ppb.Patient_DeceasedX_DateTime{ 36 | DateTime: dateTime, 37 | }, 38 | }, 39 | want: dateTime, 40 | }, 41 | { 42 | name: "", 43 | input: &opb.Observation_Component_ValueX{ 44 | Choice: &opb.Observation_Component_ValueX_StringValue{ 45 | StringValue: fhir.String("some string"), 46 | }, 47 | }, 48 | want: fhir.String("some string"), 49 | }, 50 | } 51 | 52 | for _, tc := range testCases { 53 | t.Run(tc.name, func(t *testing.T) { 54 | got := protofields.UnwrapOneofField(tc.input, "choice") 55 | 56 | if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 57 | t.Errorf("UnwrapChoiceField returned unexpected diff (-want, +got)\n%s", diff) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/protofields/strcase.go: -------------------------------------------------------------------------------- 1 | package protofields 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") 9 | var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") 10 | 11 | // toSnakeCase is a helper function to convert CamelCase names to snake_case. 12 | // This is needed for finding fields in the Proto descriptors, which are snake_case, 13 | // from resource-names that are CamelCase. 14 | // 15 | // Note: strcase.ToSnake does not work for converting Base64Binary to 16 | // base64_binary, so this function exists to do it for us with the semantics we 17 | // want. 18 | func toSnakeCase(str string) string { 19 | snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") 20 | snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") 21 | return strings.ToLower(snake) 22 | } 23 | -------------------------------------------------------------------------------- /internal/protofields/update.go: -------------------------------------------------------------------------------- 1 | package protofields 2 | 3 | import ( 4 | "fmt" 5 | 6 | "google.golang.org/protobuf/proto" 7 | "google.golang.org/protobuf/reflect/protoreflect" 8 | ) 9 | 10 | // Overwrite overwrites a field of the given name for the specified message. 11 | // If `values` is empty, the field is cleared. 12 | // If `values` contains more than one entry for a non-repeated field, this panics. 13 | func Overwrite(in proto.Message, fieldName string, values ...proto.Message) { 14 | msg := in.ProtoReflect() 15 | descriptor := msg.Descriptor() 16 | field := descriptor.Fields().ByName(protoreflect.Name(fieldName)) 17 | 18 | // No values -- remove the field entirely 19 | if len(values) == 0 { 20 | msg.Clear(field) 21 | return 22 | } 23 | 24 | // For lists, append each one after clearing the previously stored value 25 | if field.IsList() { 26 | msg.Clear(field) 27 | list := msg.Mutable(field).List() 28 | for _, v := range values { 29 | list.Append(protoreflect.ValueOfMessage(v.ProtoReflect())) 30 | } 31 | return 32 | } 33 | 34 | // For single values on non-repeated fields, just set it. 35 | if len(values) == 1 { 36 | msg.Set(field, protoreflect.ValueOfMessage(values[0].ProtoReflect())) 37 | return 38 | } 39 | 40 | panic( 41 | fmt.Sprintf( 42 | "invalid use of Overwrite; non-repeated field '%v' used with '%v' values", 43 | fieldName, 44 | len(values), 45 | ), 46 | ) 47 | } 48 | 49 | // AppendList updates a field of the given name in-place for the specified message. 50 | // 51 | // This function will panic if the field is not a repeated-field. 52 | func AppendList(in proto.Message, fieldName string, values ...proto.Message) { 53 | msg := in.ProtoReflect() 54 | descriptor := msg.Descriptor() 55 | field := descriptor.Fields().ByName(protoreflect.Name(fieldName)) 56 | 57 | list := msg.Mutable(field).List() 58 | for _, v := range values { 59 | list.Append(protoreflect.ValueOfMessage(v.ProtoReflect())) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/protofields/update_test.go: -------------------------------------------------------------------------------- 1 | package protofields_test 2 | 3 | import ( 4 | "testing" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/verily-src/fhirpath-go/internal/protofields" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | func TestOverwrite(t *testing.T) { 12 | value := &dtpb.String{ 13 | Value: "hello world", 14 | } 15 | testCases := []struct { 16 | name string 17 | field string 18 | values []proto.Message 19 | input proto.Message 20 | want proto.Message 21 | }{ 22 | { 23 | name: "Solo field", 24 | field: "text", 25 | values: []proto.Message{value}, 26 | input: &dtpb.HumanName{}, 27 | want: &dtpb.HumanName{ 28 | Text: value, 29 | }, 30 | }, { 31 | name: "Solo field no input", 32 | field: "text", 33 | values: []proto.Message{}, 34 | input: &dtpb.HumanName{ 35 | Text: value, 36 | }, 37 | want: &dtpb.HumanName{}, 38 | }, { 39 | name: "Repeated field with single input", 40 | field: "prefix", 41 | values: []proto.Message{value}, 42 | input: &dtpb.HumanName{}, 43 | want: &dtpb.HumanName{ 44 | Prefix: []*dtpb.String{value}, 45 | }, 46 | }, { 47 | name: "Repeated field with multiple inputs", 48 | field: "prefix", 49 | values: []proto.Message{value, value}, 50 | input: &dtpb.HumanName{}, 51 | want: &dtpb.HumanName{ 52 | Prefix: []*dtpb.String{value, value}, 53 | }, 54 | }, { 55 | name: "Repeated field no input", 56 | field: "prefix", 57 | values: []proto.Message{}, 58 | input: &dtpb.HumanName{ 59 | Prefix: []*dtpb.String{value, value}, 60 | }, 61 | want: &dtpb.HumanName{}, 62 | }, 63 | } 64 | 65 | for _, tc := range testCases { 66 | t.Run(tc.name, func(t *testing.T) { 67 | protofields.Overwrite(tc.input, tc.field, tc.values...) 68 | 69 | if got, want := tc.input, tc.want; !proto.Equal(got, want) { 70 | t.Errorf("Overwrite(%v): got %v, want %v", tc.name, got, want) 71 | } 72 | }) 73 | } 74 | } 75 | 76 | func TestOverwrite_WrongCardinality_Panics(t *testing.T) { 77 | defer func() { _ = recover() }() 78 | value := &dtpb.String{ 79 | Value: "hello world", 80 | } 81 | name := &dtpb.HumanName{} 82 | 83 | protofields.Overwrite(name, "text", value, value) 84 | 85 | t.Errorf("Overwrite: expected panic") 86 | } 87 | 88 | func TestAppendList(t *testing.T) { 89 | toAppend := &dtpb.String{ 90 | Value: "hello world", 91 | } 92 | value := &dtpb.String{ 93 | Value: "another string", 94 | } 95 | testCases := []struct { 96 | name string 97 | field string 98 | input proto.Message 99 | want proto.Message 100 | }{ 101 | { 102 | name: "Repeated field with no inputs", 103 | field: "prefix", 104 | input: &dtpb.HumanName{}, 105 | want: &dtpb.HumanName{ 106 | Prefix: []*dtpb.String{toAppend}, 107 | }, 108 | }, { 109 | name: "Repeated field with 1 input", 110 | field: "prefix", 111 | input: &dtpb.HumanName{ 112 | Prefix: []*dtpb.String{value}, 113 | }, 114 | want: &dtpb.HumanName{ 115 | Prefix: []*dtpb.String{value, toAppend}, 116 | }, 117 | }, { 118 | name: "Repeated field with multiple inputs", 119 | field: "prefix", 120 | input: &dtpb.HumanName{ 121 | Prefix: []*dtpb.String{value, value}, 122 | }, 123 | want: &dtpb.HumanName{ 124 | Prefix: []*dtpb.String{value, value, toAppend}, 125 | }, 126 | }, 127 | } 128 | 129 | for _, tc := range testCases { 130 | t.Run(tc.name, func(t *testing.T) { 131 | protofields.AppendList(tc.input, tc.field, toAppend) 132 | 133 | if got, want := tc.input, tc.want; !proto.Equal(got, want) { 134 | t.Errorf("AppendList(%v): got %v, want %v", tc.name, got, want) 135 | } 136 | }) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /internal/resource/canonical_identity.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | // ErrMissingCanonicalURL is thrown when creating a canonical identity without having a URL. 11 | ErrMissingCanonicalURL = errors.New("missing canonical url") 12 | 13 | delimiter = "/" 14 | ) 15 | 16 | // CanonicalIdentity is a canonical representation of a FHIR Resource. 17 | // 18 | // This object stores the individual pieces of id used in creating a canonical reference. 19 | type CanonicalIdentity struct { 20 | Version string 21 | Url string 22 | Fragment string // only used if a fragment of resource is targetted 23 | } 24 | 25 | // Type attempts to identify the resource type associated with the identity. 26 | func (c *CanonicalIdentity) Type() (Type, bool) { 27 | for _, r := range strings.Split(c.Url, delimiter) { 28 | if IsType(r) { 29 | return Type(r), true 30 | } 31 | } 32 | return Type(""), false 33 | } 34 | 35 | // String returns a string representation of this CanonicalIdentity. 36 | func (c *CanonicalIdentity) String() string { 37 | res := c.Url 38 | if c.Version != "" { 39 | res = fmt.Sprintf("%s|%s", res, c.Version) 40 | } 41 | if c.Fragment != "" { 42 | res = fmt.Sprintf("%s#%s", res, c.Fragment) 43 | } 44 | return res 45 | } 46 | 47 | // NewCanonicalIdentity creates a canonicalIdentity based on the given url, version and fragment 48 | func NewCanonicalIdentity(url, version, fragment string) (*CanonicalIdentity, error) { 49 | if url == "" { 50 | return nil, ErrMissingCanonicalURL 51 | } 52 | 53 | return &CanonicalIdentity{ 54 | Url: url, 55 | Version: version, 56 | Fragment: fragment, 57 | }, nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/resource/canonical_identity_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "github.com/verily-src/fhirpath-go/internal/resource" 8 | "google.golang.org/protobuf/testing/protocmp" 9 | ) 10 | 11 | func TestCanonicalIdentity_EmptyURL_ReturnsError(t *testing.T) { 12 | _, got := resource.NewCanonicalIdentity("", "v1", "") 13 | 14 | if got != resource.ErrMissingCanonicalURL { 15 | t.Errorf("NewCanonicalIdentity: got %v, want %v", got, resource.ErrMissingCanonicalURL) 16 | } 17 | } 18 | 19 | func TestCanonicalIdentity(t *testing.T) { 20 | testCases := []struct { 21 | name, url, version, fragment string 22 | want *resource.CanonicalIdentity 23 | wantString string 24 | wantType resource.Type 25 | hasType bool 26 | }{ 27 | { 28 | name: "basic", 29 | url: "http://someurl/test-value", 30 | wantString: "http://someurl/test-value", 31 | }, 32 | { 33 | name: "long url", 34 | url: "https://fhir.acme.com/Questionnaire/example", 35 | wantString: "https://fhir.acme.com/Questionnaire/example", 36 | hasType: true, 37 | wantType: resource.Questionnaire, 38 | }, 39 | { 40 | name: "with version", 41 | url: "https://fhir.acme.com/PlanDefinition/example", 42 | version: "1.0.0", 43 | wantString: "https://fhir.acme.com/PlanDefinition/example|1.0.0", 44 | hasType: true, 45 | wantType: resource.PlanDefinition, 46 | }, 47 | { 48 | name: "with fragment", 49 | url: "http://hl7.org/fhir/ValueSet/my-valueset", 50 | fragment: "vs1", 51 | wantString: "http://hl7.org/fhir/ValueSet/my-valueset#vs1", 52 | }, 53 | { 54 | name: "with version and fragment", 55 | url: "http://fhir.acme.com/ActivityDefinition/example", 56 | version: "1.0", 57 | fragment: "vs1", 58 | wantString: "http://fhir.acme.com/ActivityDefinition/example|1.0#vs1", 59 | hasType: true, 60 | wantType: resource.ActivityDefinition, 61 | }, 62 | } 63 | 64 | for _, tc := range testCases { 65 | t.Run(tc.name, func(t *testing.T) { 66 | got, _ := resource.NewCanonicalIdentity(tc.url, tc.version, tc.fragment) 67 | want := &resource.CanonicalIdentity{ 68 | Url: tc.url, 69 | Version: tc.version, 70 | Fragment: tc.fragment, 71 | } 72 | 73 | if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { 74 | t.Errorf("CanonicalIdentity(%s): %v", tc.name, diff) 75 | } 76 | 77 | if s := got.String(); tc.wantString != s { 78 | t.Errorf("CanonicalIdentity(%s).String: want: %s, got: %s", tc.name, tc.wantString, s) 79 | } 80 | if tc.hasType { 81 | gt, ok := got.Type() 82 | if !ok || gt != tc.wantType { 83 | t.Errorf("CanonicalIdentity(%s).Type: want: %s, got: %s", tc.name, tc.wantType, gt) 84 | } 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/resource/contactable/contactable.go: -------------------------------------------------------------------------------- 1 | // Package contactable contains utilities for working with FHIR Resource objects that 2 | // include a contact field 3 | package contactable 4 | 5 | import ( 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/verily-src/fhirpath-go/internal/fhir" 8 | "github.com/verily-src/fhirpath-go/internal/resource" 9 | "github.com/verily-src/fhirpath-go/internal/resourceopt" 10 | ) 11 | 12 | // Option is an option that may be supplied to updates of ContactableResource types 13 | type Option = resourceopt.Option 14 | 15 | // ContactableResource is the interface for FHIR resources that include a contact field 16 | type ContactableResource interface { 17 | GetContact() []*dtpb.ContactDetail 18 | fhir.DomainResource 19 | } 20 | 21 | // WithContacts returns a resource Option for setting the ContactableResource 22 | // Contact with the specified contact entry. 23 | func WithContacts(contact ...*dtpb.ContactDetail) Option { 24 | return resourceopt.WithProtoField("contact", contact...) 25 | } 26 | 27 | // Update modifies the input resource in-place with the specified options. 28 | func Update(cr ContactableResource, opts ...Option) { 29 | resource.Update(cr.(fhir.Resource), opts...) 30 | } 31 | -------------------------------------------------------------------------------- /internal/resource/contactable/contactable_test.go: -------------------------------------------------------------------------------- 1 | package contactable_test 2 | 3 | import ( 4 | "testing" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/verily-src/fhirpath-go/internal/fhir" 8 | "github.com/verily-src/fhirpath-go/internal/fhirtest" 9 | "github.com/verily-src/fhirpath-go/internal/resource/contactable" 10 | "google.golang.org/protobuf/proto" 11 | ) 12 | 13 | func TestWithContact(t *testing.T) { 14 | want := &dtpb.ContactDetail{ 15 | Name: fhir.String("deadbeef"), 16 | } 17 | 18 | for name, res := range fhirtest.CanonicalResources { 19 | t.Run(name, func(t *testing.T) { 20 | got := proto.Clone(res).(contactable.ContactableResource) 21 | 22 | contactable.Update(got, contactable.WithContacts(want)) 23 | 24 | for _, got := range got.(fhir.CanonicalResource).GetContact() { 25 | if !proto.Equal(got, want) { 26 | t.Errorf("WithContact(%v): got %v, want %v", name, got, want) 27 | } 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/resource/identity_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/verily-src/fhirpath-go/internal/fhirtest" 9 | "github.com/verily-src/fhirpath-go/internal/resource" 10 | ) 11 | 12 | func TestIdentityOf_NilInputs_ReturnsNoValue(t *testing.T) { 13 | _, got := resource.IdentityOf(nil) 14 | 15 | if got, want := got, false; got != want { 16 | t.Errorf("IdentityOf: got %v, want %v", got, want) 17 | } 18 | } 19 | 20 | func TestIdentityOf(t *testing.T) { 21 | for name, res := range fhirtest.Resources { 22 | t.Run(name, func(t *testing.T) { 23 | want, err := resource.NewIdentity( 24 | string(resource.TypeOf(res)), 25 | res.GetId().GetValue(), 26 | res.GetMeta().GetVersionId().GetValue(), 27 | ) 28 | if err != nil { 29 | t.Fatalf("IdentityOf(%v): got unexpected err: %v", name, err) 30 | } 31 | 32 | got, ok := resource.IdentityOf(res) 33 | if !ok { 34 | t.Fatalf("IdentityOf(%v): got false for ok", name) 35 | } 36 | 37 | if !cmp.Equal(got, want) { 38 | t.Errorf("IdentityOf(%v): got %v, want %v", name, got, want) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestNewIdentity_BadInput_ReturnsErrBadType(t *testing.T) { 45 | _, err := resource.NewIdentity("", "1234", "5678") 46 | 47 | if got, want := err, resource.ErrBadType; !errors.Is(got, want) { 48 | t.Errorf("NewIdentity: got err '%v', want err '%v'", got, want) 49 | } 50 | } 51 | 52 | func TestNewIdentityFromURL(t *testing.T) { 53 | testCases := []struct { 54 | name string 55 | URL string 56 | wantIdentity *resource.Identity 57 | }{ 58 | { 59 | "URL", 60 | "https://healthcare.googleapis.com/v1/projects/my-project-name/locations/us-east4/datasets/my-dataset-name/fhirStores/my-fhir-store-name/fhir/Binary/123", 61 | mustNewIdentity("Binary", "123", ""), 62 | }, 63 | { 64 | "URILong", 65 | "projects/my-project-name/locations/us-east4/datasets/my-dataset-name/fhirStores/my-fhir-store-name/fhir/Patient/abc", 66 | mustNewIdentity("Patient", "abc", ""), 67 | }, 68 | { 69 | "URIShort", 70 | "Patient/abc", 71 | mustNewIdentity("Patient", "abc", ""), 72 | }, 73 | { 74 | "Invalid", 75 | "ThisIsNotAValidResourceName", 76 | nil, 77 | }, 78 | } 79 | for _, tc := range testCases { 80 | t.Run(tc.name, func(t *testing.T) { 81 | gotIdentity, _ := resource.NewIdentityFromURL(tc.URL) 82 | if !cmp.Equal(gotIdentity, tc.wantIdentity) { 83 | t.Errorf("WithNewIdentityFromURL: got %v, want %v", gotIdentity, tc.wantIdentity) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func TestNewIdentityFromHistoryURL(t *testing.T) { 90 | testCases := []struct { 91 | name string 92 | historyUrl string 93 | expectedValue *resource.Identity 94 | }{ 95 | { 96 | "URL", 97 | "https://healthcare.googleapis.com/v1/projects/my-project-name/locations/us-east4/datasets/my-dataset-name/fhirStores/my-fhir-store-name/fhir/Binary/123/_history/456", 98 | mustNewIdentity("Binary", "123", "456"), 99 | }, 100 | { 101 | "URI", 102 | "projects/my-project-name/locations/us-east4/datasets/my-dataset-name/fhirStores/my-fhir-store-name/fhir/Patient/abc/_history/def", 103 | mustNewIdentity("Patient", "abc", "def"), 104 | }, 105 | { 106 | "Invalid", 107 | "ThisIsNotAValidResourceName", 108 | nil, 109 | }, 110 | } 111 | for _, tc := range testCases { 112 | t.Run(tc.name, func(t *testing.T) { 113 | result, _ := resource.NewIdentityFromHistoryURL(tc.historyUrl) 114 | if ((result != nil) != (tc.expectedValue != nil)) || 115 | (result != nil && tc.expectedValue != nil && *result != *tc.expectedValue) { 116 | t.Errorf("%s: Got = %v, want = %v", tc.name, result, tc.expectedValue) 117 | } 118 | }) 119 | } 120 | } 121 | 122 | func TestIdentityEqual(t *testing.T) { 123 | identityA := mustNewIdentity("Patient", "A", "v1") 124 | testCases := []struct { 125 | name string 126 | lhs *resource.Identity 127 | rhs *resource.Identity 128 | wantEqual bool 129 | }{ 130 | {"both nil", nil, nil, true}, 131 | {"lhs nil", nil, identityA, false}, 132 | {"rhs nil", identityA, nil, false}, 133 | {"same", identityA, mustNewIdentity("Patient", "A", "v1"), true}, 134 | {"different type", identityA, mustNewIdentity("Person", "A", "v1"), false}, 135 | {"different id", identityA, mustNewIdentity("Patient", "B", "v1"), false}, 136 | {"different version", identityA, mustNewIdentity("Patient", "A", "v2"), false}, 137 | } 138 | for _, tc := range testCases { 139 | t.Run(tc.name, func(t *testing.T) { 140 | gotEqual := tc.lhs.Equal(tc.rhs) 141 | if gotEqual != tc.wantEqual { 142 | t.Errorf("Equal(%s) got %v want %v", tc.name, gotEqual, tc.wantEqual) 143 | } 144 | }) 145 | } 146 | } 147 | 148 | func TestIdentity_Unversioned(t *testing.T) { 149 | withVersion := mustNewIdentity("Patient", "123", "abc") 150 | got := withVersion.Unversioned() 151 | want := mustNewIdentity("Patient", "123", "") 152 | if !cmp.Equal(got, want) { 153 | t.Errorf("Unversioned: got %v, want %v", got, want) 154 | } 155 | } 156 | 157 | func mustNewIdentity(resourceType, id, versionID string) *resource.Identity { 158 | identity, err := resource.NewIdentity(resourceType, id, versionID) 159 | if err != nil { 160 | panic(err) 161 | } 162 | return identity 163 | } 164 | 165 | func TestWithNewVersion(t *testing.T) { 166 | originalIdentity := mustNewIdentity("Patient", "foo", "") 167 | wantIdentity := mustNewIdentity("Patient", "foo", "bar") 168 | 169 | gotIdentity := originalIdentity.WithNewVersion("bar") 170 | 171 | if !cmp.Equal(gotIdentity, wantIdentity) { 172 | t.Errorf("WithNewVersion: got %v, want %v", gotIdentity, wantIdentity) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /internal/resource/options.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 5 | "github.com/verily-src/fhirpath-go/internal/fhir" 6 | "github.com/verily-src/fhirpath-go/internal/resourceopt" 7 | ) 8 | 9 | // WithMeta returns a resource Option for setting the Resource Meta with the 10 | // specified meta entry. 11 | func WithMeta(meta *dtpb.Meta) Option { 12 | return resourceopt.WithProtoField("meta", meta) 13 | } 14 | 15 | // WithID returns a resource Option for setting the Resourec ID with the id of 16 | // the provided string. 17 | func WithID(id string) Option { 18 | return resourceopt.WithProtoField("id", fhir.ID(id)) 19 | } 20 | 21 | // WithImplicitRules returns a resource Option for setting the Resource implicit 22 | // rules with the provided string. 23 | func WithImplicitRules(rules string) Option { 24 | return resourceopt.WithProtoField("implicit_rules", fhir.URI(rules)) 25 | } 26 | 27 | // WithLanguage returns a resource Option for setting the Resource language to 28 | // the code of the provided string. 29 | func WithLanguage(language string) Option { 30 | return resourceopt.WithProtoField("language", fhir.Code(language)) 31 | } 32 | -------------------------------------------------------------------------------- /internal/resource/options_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "testing" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/verily-src/fhirpath-go/internal/fhir" 8 | "github.com/verily-src/fhirpath-go/internal/fhirtest" 9 | "github.com/verily-src/fhirpath-go/internal/resource" 10 | "google.golang.org/protobuf/proto" 11 | ) 12 | 13 | func TestWithMeta(t *testing.T) { 14 | want := &dtpb.Meta{ 15 | VersionId: &dtpb.Id{ 16 | Value: "deadbeef", 17 | }, 18 | } 19 | 20 | for name, res := range fhirtest.Resources { 21 | t.Run(name, func(t *testing.T) { 22 | got := proto.Clone(res).(fhir.Resource) 23 | 24 | resource.Update(got, resource.WithMeta(want)) 25 | 26 | if got, want := got.GetMeta(), want; !proto.Equal(got, want) { 27 | t.Errorf("WithMeta(%v): got %v, want %v", name, got, want) 28 | } 29 | }) 30 | } 31 | } 32 | 33 | func TestWithID(t *testing.T) { 34 | const id = "123456789" 35 | want := fhir.ID(id) 36 | 37 | for name, res := range fhirtest.Resources { 38 | t.Run(name, func(t *testing.T) { 39 | got := proto.Clone(res).(fhir.Resource) 40 | 41 | resource.Update(got, resource.WithID(id)) 42 | 43 | if got, want := got.GetId(), want; !proto.Equal(got, want) { 44 | t.Errorf("WithID(%v): got %v, want %v", name, got, want) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestWithImplicitRules(t *testing.T) { 51 | const rules = "https://example.com/some/rules" 52 | want := fhir.URI(rules) 53 | 54 | for name, res := range fhirtest.Resources { 55 | t.Run(name, func(t *testing.T) { 56 | got := proto.Clone(res).(fhir.Resource) 57 | 58 | resource.Update(got, resource.WithImplicitRules(rules)) 59 | 60 | if got, want := got.GetImplicitRules(), want; !proto.Equal(got, want) { 61 | t.Errorf("WithImplicitRules(%v): got %v, want %v", name, got, want) 62 | } 63 | }) 64 | } 65 | } 66 | 67 | func TestWithLanguage(t *testing.T) { 68 | const language = "en-gb" 69 | want := fhir.Code(language) 70 | 71 | for name, res := range fhirtest.Resources { 72 | t.Run(name, func(t *testing.T) { 73 | got := proto.Clone(res).(fhir.Resource) 74 | 75 | resource.Update(got, resource.WithLanguage(language)) 76 | 77 | if got, want := got.GetLanguage(), want; !proto.Equal(got, want) { 78 | t.Errorf("WithLanguage(%v): got %v, want %v", name, got, want) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/resource/resource_example_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 7 | "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/resources/patient_go_proto" 8 | "github.com/verily-src/fhirpath-go/internal/fhir" 9 | "github.com/verily-src/fhirpath-go/internal/resource" 10 | ) 11 | 12 | func ExampleGetIdentifierList() { 13 | patient := &patient_go_proto.Patient{ 14 | Id: fhir.ID("12345"), 15 | Identifier: []*dtpb.Identifier{ 16 | &dtpb.Identifier{ 17 | System: &dtpb.Uri{Value: "http://fake.com"}, 18 | Value: &dtpb.String{Value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8"}, 19 | }, 20 | }, 21 | } 22 | 23 | ids, err := resource.GetIdentifierList(patient) 24 | if err != nil { 25 | panic(err) 26 | } else if ids == nil || len(ids) == 0 { 27 | panic("no identifiers") 28 | } else { 29 | fmt.Printf("Identifier value: %#v", ids[0].GetValue().Value) 30 | // Output: Identifier value: "9efbf82d-7a58-4d14-bec1-63f8fda148a8" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/resource/type.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | dtpb "github.com/google/fhir/go/proto/google/fhir/proto/r4/core/datatypes_go_proto" 8 | "github.com/verily-src/fhirpath-go/internal/fhir" 9 | "github.com/verily-src/fhirpath-go/internal/protofields" 10 | ) 11 | 12 | // ErrBadType is an error raised when a bad type is provided. 13 | var ErrBadType = errors.New("bad resource type") 14 | 15 | // Type is a FHIR Resource type object. This is similar to a reflect.Type that 16 | // encodes its name identifier 17 | // 18 | // Type objects should never be constructed manually; rather, use the `CheckType` 19 | // or `TypeOf` functions to get a valid type object. Invalid instances of Type 20 | // may lead to unexpected implicit `panic` behavior, as any code consuming this 21 | // is allowed to assume that `Type` always names a valid instance. 22 | type Type string 23 | 24 | // TypeOf gets the underlying type of the named resource. 25 | // 26 | // This function panics if resource is nil. Note that this is only an issue if 27 | // the interface `fhir.Resource` is nil, *not* if the underlying resource is a 28 | // pointer that is nil. E.g. the following holds true: 29 | // 30 | // assert.True(resource.TypeOf((*ppb.Patient)(nil)) == resource.Patient) 31 | func TypeOf(resource fhir.Resource) Type { 32 | if resource == nil { 33 | panic("TypeOf provided nil Resource") 34 | } 35 | return Type(resource.ProtoReflect().Descriptor().Name()) 36 | } 37 | 38 | // NewType checks whether the string type name is a valid resource.Type instance. 39 | // If it is, an instance of the type is returned. If the provided type is not a 40 | // valid type, an ErrBadType is returned, and the type result is garbage. 41 | // 42 | // Note: This is case-sensitive, and expects CamelCase, just as the FHIR spec uses. 43 | func NewType(resourceType string) (Type, error) { 44 | if !IsType(resourceType) { 45 | return "", fmt.Errorf("%w '%v'", ErrBadType, resourceType) 46 | } 47 | return Type(resourceType), nil 48 | } 49 | 50 | // String converts this Type into a string. 51 | func (t Type) String() string { 52 | return string(t) 53 | } 54 | 55 | // New returns an instance of the FHIR Resource which this type names, using 56 | // the provided options to toggle. 57 | // 58 | // This function will panic if this does not name a valid Resource Type. 59 | func (t Type) New(opts ...Option) fhir.Resource { 60 | return New(t, opts...) 61 | } 62 | 63 | // URI returns a URI object containing the resource type name. 64 | func (t Type) URI() *dtpb.Uri { 65 | return &dtpb.Uri{ 66 | Value: string(t), 67 | } 68 | } 69 | 70 | // StructureDefinitionURI returns an absolute URI to the structure-definition 71 | // URL. 72 | func (t Type) StructureDefinitionURI() *dtpb.Uri { 73 | const baseURL = "http://hl7.org/fhir/StructureDefinition" 74 | 75 | return &dtpb.Uri{ 76 | Value: fmt.Sprintf("%v/%v", baseURL, t), 77 | } 78 | } 79 | 80 | // IsType queries whether the given string names a Resource type. 81 | // 82 | // Note: This is case-sensitive, and expects CamelCase, jus as the FHIR spec uses. 83 | func IsType(name string) bool { 84 | _, ok := protofields.Resources[name] 85 | return ok 86 | } 87 | -------------------------------------------------------------------------------- /internal/resource/type_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/verily-src/fhirpath-go/internal/fhirtest" 10 | "github.com/verily-src/fhirpath-go/internal/resource" 11 | ) 12 | 13 | func TestTypeOf_ReturnsType(t *testing.T) { 14 | for name, res := range fhirtest.Resources { 15 | t.Run(name, func(t *testing.T) { 16 | want := string(res.ProtoReflect().Descriptor().Name()) 17 | 18 | got := resource.TypeOf(res) 19 | 20 | if !cmp.Equal(string(got), want) { 21 | t.Errorf("TypeOf(%v): got '%v', want '%v'", name, got, want) 22 | } 23 | }) 24 | } 25 | } 26 | 27 | func TestTypeOf_NilInput_Panics(t *testing.T) { 28 | defer func() { _ = recover() }() 29 | 30 | resource.TypeOf(nil) 31 | 32 | t.Errorf("TypeOf: expected panic") 33 | } 34 | 35 | func TestNewType_ValidTypeName_ReturnsType(t *testing.T) { 36 | for name, res := range fhirtest.Resources { 37 | t.Run(name, func(t *testing.T) { 38 | want := resource.TypeOf(res) 39 | 40 | got, err := resource.NewType(name) 41 | if err != nil { 42 | t.Fatalf("NewType: got unexpected err '%v' from NewType", err) 43 | } 44 | 45 | if !cmp.Equal(got, want) { 46 | t.Errorf("NewType(%v): got %v, want %v", name, got, want) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func TestNewType_InvalidTypeName_ReturnsErrBadType(t *testing.T) { 53 | testCases := []struct { 54 | name string 55 | value string 56 | }{ 57 | {"Empty", ""}, 58 | {"NotAnElement", "Bad-Element"}, 59 | {"AnonymousElement", "Bundle_Entry"}, 60 | } 61 | 62 | for _, tc := range testCases { 63 | t.Run(tc.name, func(t *testing.T) { 64 | _, err := resource.NewType(tc.value) 65 | 66 | if got, want := err, resource.ErrBadType; !errors.Is(got, want) { 67 | t.Errorf("NewType(%v): got %v, want %v", tc.name, got, want) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestIsType_ValidTypeName_ReturnsTrue(t *testing.T) { 74 | for name := range fhirtest.Resources { 75 | t.Run(name, func(t *testing.T) { 76 | got := resource.IsType(name) 77 | 78 | if got != true { 79 | t.Errorf("IsType(%v): got %v, want true", name, got) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestIsType_InvalidTypeName_ReturnsFalse(t *testing.T) { 86 | testCases := []struct { 87 | name string 88 | value string 89 | }{ 90 | {"Empty", ""}, 91 | {"NotAResource", "ContainedResource"}, 92 | {"Element", "String"}, 93 | } 94 | 95 | for _, tc := range testCases { 96 | t.Run(tc.name, func(t *testing.T) { 97 | got := resource.IsType(tc.value) 98 | 99 | if got != false { 100 | t.Errorf("IsType(%v): got %v, want false", tc.name, got) 101 | } 102 | }) 103 | } 104 | } 105 | 106 | func TestTypeNew_ReturnsElementOfType(t *testing.T) { 107 | for name, elem := range fhirtest.Resources { 108 | t.Run(name, func(t *testing.T) { 109 | ty := resource.TypeOf(elem) 110 | want := reflect.TypeOf(elem) 111 | 112 | got := ty.New() 113 | 114 | if reflect.TypeOf(got) != want { 115 | t.Errorf("Type.New: got %v, want %v", got, want) 116 | } 117 | }) 118 | } 119 | } 120 | 121 | func TestTypeNew_Unspecified_ReturnsNil(t *testing.T) { 122 | defer func() { _ = recover() }() 123 | 124 | resource.Type("").New() 125 | 126 | t.Errorf("Type.New: expected panic") 127 | } 128 | -------------------------------------------------------------------------------- /internal/resourceopt/resourceopt.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package resourceopt is an internal package that provides helper utilities 3 | for forming resource-options in resource packages. 4 | */ 5 | package resourceopt 6 | 7 | import ( 8 | "github.com/verily-src/fhirpath-go/internal/fhir" 9 | "github.com/verily-src/fhirpath-go/internal/protofields" 10 | "github.com/verily-src/fhirpath-go/internal/slices" 11 | "google.golang.org/protobuf/proto" 12 | ) 13 | 14 | // Option is the definition of a resource Option used for creating and updating 15 | // FHIR Resources. 16 | type Option interface { 17 | update(fhir.Resource) 18 | } 19 | 20 | // ApplyOptions applies the specified options to the input resource. 21 | // 22 | // This function is defined here due to the Option interface providing an 23 | // unexported field. This is needed so that the other packages using this can 24 | // accumulate the options without having access to the unexported call. 25 | func ApplyOptions[T fhir.Resource, O Option](r T, opts ...O) T { 26 | for _, opt := range opts { 27 | opt.update(r) 28 | } 29 | return r 30 | } 31 | 32 | // WithProtoField is a resource Option that sets the specified 'field' in the proto 33 | // to the values. If values is empty, the field is cleared. If values is 34 | // not 1, and a field is not repeated, this functon will panic. 35 | // 36 | // Note: This is an internal function intended to be used to form generic 37 | // resource options that will work with all FHIR resources. 38 | func WithProtoField[T proto.Message](fieldName string, values ...T) Option { 39 | // SAFETY: 40 | // MustConvert cannot fail here, since the 'T' constraint above ensures that 41 | // all inputs will be valid proto.Message types. 42 | return withProtoFieldImpl(fieldName, slices.MustConvert[proto.Message](values)...) 43 | } 44 | 45 | func withProtoFieldImpl(fieldName string, values ...proto.Message) Option { 46 | return WithCallback(func(r fhir.Resource) { 47 | protofields.Overwrite(r, fieldName, values...) 48 | }) 49 | } 50 | 51 | // IncludeProtoField is a resource Option that appends the specified entries to 52 | // the given 'field' in the proto. This function will panic if the given field 53 | // is not a repeated field in the proto. 54 | // 55 | // Note: This is an internal function intended to be used to form generic 56 | // resource options that will work with all FHIR resources. 57 | func IncludeProtoField[T proto.Message](fieldName string, values ...T) Option { 58 | // SAFETY: 59 | // MustConvert cannot fail here, since the 'T' constraint above ensures that 60 | // all inputs will be valid proto.Message types. 61 | return includeProtoFieldImpl(fieldName, slices.MustConvert[proto.Message](values)...) 62 | } 63 | 64 | func includeProtoFieldImpl(fieldName string, values ...proto.Message) Option { 65 | return WithCallback(func(r fhir.Resource) { 66 | protofields.AppendList(r, fieldName, values...) 67 | }) 68 | } 69 | 70 | // WithCallback returns a resource Option that simply passes the resource being 71 | // created back into the specified callback. This exists to be built into 72 | // larger, more strongly-typed options. 73 | func WithCallback[T fhir.Resource](callback func(T)) Option { 74 | return &callbackOpt[T]{callback} 75 | } 76 | 77 | type callbackOpt[T fhir.Resource] struct { 78 | callback func(T) 79 | } 80 | 81 | func (o *callbackOpt[T]) update(r fhir.Resource) { 82 | o.callback(r.(T)) 83 | } 84 | -------------------------------------------------------------------------------- /internal/stablerand/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package stablerand is a small helper utility that encapsulates its random engine 3 | and always uses the same seed value for its randomness. 4 | 5 | This ensures reproducibility and stability across executions, giving a 6 | pseudo-random distribution, but with deterministic predictability. This is 7 | primarily intended for generating content for tests, which ensures that inputs 8 | are still pseudo-random, but predictible and consistent across unchanged 9 | executions. 10 | 11 | Functions in this package are thread-safe, although use in threaded contexts 12 | will remove any guarantees of determinism. 13 | 14 | Note: This is primarily used internally for the fhirtest package to implement 15 | "random" IDs and meta-IDs so that test resources retain the same general 16 | values across executions. 17 | */ 18 | package stablerand 19 | -------------------------------------------------------------------------------- /internal/stablerand/rand.go: -------------------------------------------------------------------------------- 1 | package stablerand 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | var ( 10 | // stableRand is the random engine used for generating random data in package 11 | // fhirtest. 12 | stableRand *rand.Rand 13 | 14 | // randMutex provides thread-safety for stableRand, in case any tests are 15 | // executed with t.Parallel(). Parallelism will affect the stability of the 16 | // randomness, since the generated values will no longer be deterministic once 17 | // concurrency is involved; but this doesn't mean the code should fail. 18 | randMutex sync.Mutex 19 | ) 20 | 21 | const ( 22 | // randSeed is the seed used for the random engine used in package fhirtest. 23 | // This seed is constant so that subsequent test executions will always receive 24 | // the same data. 25 | randSeed = 0xbadc0ffee 26 | 27 | // alnumAlphabet is a string containing all the upper and lowercase ascii 28 | // characters for letters and digits. 29 | alnumAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 30 | 31 | // hexAlphabet is a string containing lowercase hex characters. 32 | hexAlphabet = "abcdef0123456789" 33 | 34 | // decAlphabet is a string containing all decimal ascii characters. 35 | decAlphabet = "0123456789" 36 | ) 37 | 38 | func init() { 39 | // Seed the random engine with a static value so that generation is consistent 40 | // across test executions, but still produces "unique" values. 41 | stableRand = rand.New(rand.NewSource(randSeed)) 42 | } 43 | 44 | // Intn returns, as an int, a non-negative pseudo-random number in the half-open 45 | // interval [0,n). It panics if n <= 0. 46 | func Intn(n int) int { 47 | randMutex.Lock() 48 | defer randMutex.Unlock() 49 | 50 | return stableRand.Intn(n) 51 | } 52 | 53 | // Int63n returns, as an int64, a non-negative pseudo-random number in the 54 | // half-open interval [0,n). It panics if n <= 0. 55 | func Int63n(n int64) int64 { 56 | randMutex.Lock() 57 | defer randMutex.Unlock() 58 | 59 | return stableRand.Int63n(n) 60 | } 61 | 62 | // String returns, as a string, a pseudo-random string containing n characters 63 | // all consisting of values within the supplied alphabet string. 64 | // It panics if the alphabet string is empty. 65 | func String(n int, alphabet string) string { 66 | if alphabet == "" { 67 | panic("No alphabet specified") 68 | } 69 | randMutex.Lock() 70 | defer randMutex.Unlock() 71 | b := make([]rune, n) 72 | for i := range b { 73 | b[i] = rune(alphabet[stableRand.Intn(len(alphabet))]) 74 | } 75 | return string(b) 76 | } 77 | 78 | // AlnumString returns, as a string, a pseudo-random string containing n 79 | // alphanumeric characters. 80 | func AlnumString(n int) string { 81 | return String(n, alnumAlphabet) 82 | } 83 | 84 | // HexString returns, as a string, a pseudo-random string containing n 85 | // hex characters. 86 | func HexString(n int) string { 87 | return String(n, hexAlphabet) 88 | } 89 | 90 | // DecString returns, as a string, a pseudo-random string containing n 91 | // decimal characters. 92 | func DecString(n int) string { 93 | return String(n, decAlphabet) 94 | } 95 | 96 | // Time returns, as a time.Time object, a pseudo-random time starting with the 97 | // base time, and adding a random amount between the half-open interval 98 | // [0, variation) to the time. It panics if variation is negative. 99 | func Time(base time.Time, variation time.Duration) time.Time { 100 | randMutex.Lock() 101 | defer randMutex.Unlock() 102 | 103 | offset := time.Duration(stableRand.Int63n(int64(variation))) 104 | base.Add(offset) 105 | return base 106 | } 107 | 108 | // OneOf returns, as a T object, a pseudo-randomly selected value from args. 109 | // It panics if args is empty. 110 | func OneOf[T any](args ...T) T { 111 | if len(args) == 0 { 112 | panic("No arguments specified to OneOf") 113 | } 114 | randMutex.Lock() 115 | defer randMutex.Unlock() 116 | 117 | i := stableRand.Intn(len(args)) 118 | 119 | return args[i] 120 | } 121 | -------------------------------------------------------------------------------- /internal/units/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package units provides basic unit constants that are used for various FHIR 3 | Quantity types. 4 | */ 5 | package units 6 | -------------------------------------------------------------------------------- /internal/units/time.go: -------------------------------------------------------------------------------- 1 | package units 2 | 3 | import "fmt" 4 | 5 | // Time is a unit of measure for measuring the passage of time. 6 | type Time int 7 | 8 | const ( 9 | // Nanoseconds is a Time unit that measures time in nanoseconds. 10 | Nanoseconds Time = iota 11 | 12 | // Microseconds is a Time unit that measures time in microseconds. 13 | Microseconds 14 | 15 | // Milliseconds is a Time unit that measures time in milliseconds. 16 | Milliseconds 17 | 18 | // Seconds is a Time unit that measures time in seconds. 19 | Seconds 20 | 21 | // Minutes is a Time unit that measures time in minutes. 22 | Minutes 23 | 24 | // Hours is a Time unit that measures time in hours. 25 | Hours 26 | 27 | // Days is a Time unit that measures time in days. 28 | Days 29 | ) 30 | 31 | const ( 32 | nanosecondSymbol = "ns" 33 | microsecondSymbol = "us" 34 | millisecondSymbol = "ms" 35 | secondsSymbol = "s" 36 | minutesSymbol = "min" 37 | hoursSymbol = "h" 38 | daysSymbol = "d" 39 | ) 40 | 41 | // Symbol returns the symbol used to represent the underlying unit. 42 | func (t Time) Symbol() string { 43 | switch t { 44 | case Nanoseconds: 45 | return nanosecondSymbol 46 | case Microseconds: 47 | return microsecondSymbol 48 | case Milliseconds: 49 | return millisecondSymbol 50 | case Seconds: 51 | return secondsSymbol 52 | case Minutes: 53 | return minutesSymbol 54 | case Hours: 55 | return hoursSymbol 56 | case Days: 57 | return daysSymbol 58 | } 59 | // This is a closed enumeration in an internal package. If this panic ever 60 | // gets reached, it means that a developer is using this package wrong. 61 | panic(fmt.Sprintf("invalid time value %v", t)) 62 | } 63 | 64 | // System returns the time system that this unit comes from. 65 | func (t Time) System() string { 66 | return "http://unitsofmeasure.org" 67 | } 68 | 69 | // TimeFromSymbol creates the Time object 70 | func TimeFromSymbol(symbol string) (Time, error) { 71 | switch symbol { 72 | case nanosecondSymbol: 73 | return Nanoseconds, nil 74 | case microsecondSymbol: 75 | return Microseconds, nil 76 | case millisecondSymbol: 77 | return Milliseconds, nil 78 | case secondsSymbol: 79 | return Seconds, nil 80 | case minutesSymbol: 81 | return Minutes, nil 82 | case hoursSymbol: 83 | return Hours, nil 84 | case daysSymbol: 85 | return Days, nil 86 | } 87 | return Time(0), fmt.Errorf("unknown Time symbol '%v'", symbol) 88 | } 89 | --------------------------------------------------------------------------------