Skip to content

Commit

Permalink
Project import generated by Copybara.
Browse files Browse the repository at this point in the history
GitOrigin-RevId: b8bebf065c6b2212c17d7d4dbfb8e5427b9e96d7
  • Loading branch information
Copybara authored and alexlaurinmath committed May 6, 2024
0 parents commit e29f6e5
Show file tree
Hide file tree
Showing 192 changed files with 43,115 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Note: For syntax, see <https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-syntax>

* @verily-src/fhirpathgo-eng-reviewers
24 changes: 24 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Build and Test
on:
pull_request:
branches:
- main

jobs:
build-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
# Use the central Go version defined in go.mod to make it easier
# to perform upgrades.
go-version-file: go.mod
- name: Vet
run: go vet -v -unreachable=false ./...
- name: Build
run: go build -v ./...
- name: Test
run: go test ./...
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# FHIRPath

This package contains a Go implementation of the [FHIRPath][fhirpath] specification, implemented directly with
the [google/fhir][google-fhir] proto definitions.

This package aims to be compliant with both:

- the [N1 Normative Release](http://hl7.org/fhirpath/N1/) specification, and
- the [R4 specifications](http://hl7.org/fhir/R4/fhirpath.html).

## Import

```go
import "github.com/verily-src/fhirpath-go/fhirpath"
```

## Usage

A FHIRPath must be compiled before running it against a resource using the `Compile` method like so:

```go
expression, err := fhirpath.Compile("Patient.name.given")
if err != nil {
panic("error while compiling FHIRPath")
}
```

The compilation result can then be run against a resource:

```go
inputResources := []fhir.Resource{somePatient, someMedication}

result, err := expression.Evaluate(inputResources)
if err != nil {
panic("error while running FHIRPath against resource")
}
```

As defined in the FHIRPath specification, the output of evaluation is a **Collection**. So, the
result of Evaluate is of type `[]any`. As such, the result must be unpacked and cast to the desired
type for further processing.

### CompileOptions and EvaluateOptions

Options are provided for optional modification of compilation and evaluation. There is currently
support for:

- adding custom functions during Compile time
- adding custom external constant variables

#### To add a custom function

The constraints on the custom function are as follows:

- First argument must be `system.Collection`
- Arguments that follow must be either a fhir proto type or primitive system type

```go
customFn := func (input system.Collection, args ...any) (system.Collection error) {
fmt.Print("called custom fn")
return input, nil
}
expression, err := fhirpath.Compile("print()", WithFunction("print", customFn))
```

#### To add external constants

The constraints on external constants are as follows:

- Must be a fhir proto type, primitive system type, or `system.Collection`
- If you pass in a collection, contained elements must be fhir proto or system type.

```go
customVar := system.String("custom variable")
result, err := expression.Evaluate([]fhir.Resource{someResource}, WithConstant("var", customVar))
```

### System Types

The FHIRPath [spec](http://hl7.org/fhirpath/N1/#literals) defines the following custom System types:

- Boolean
- String
- Integer
- Decimal
- Quantity
- Date
- Time
- DateTime

FHIR Protos get implicitly converted to the above types according to this
[chart](http://hl7.org/fhir/R4/fhirpath.html#types), when used in some FHIRPath expressions.

### Things to be aware of

FHIRPath is not the most intuitive language, and there are some quirks. See [gotchas](gotchas.md).

[fhirpath]: http://hl7.org/fhirpath/
[google-fhir]: https://github.com/google/fhir
4 changes: 4 additions & 0 deletions fhirpath/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*antlr*.jar
*.interp
*.tokens
*fhirpath_base_visitor.go
55 changes: 55 additions & 0 deletions fhirpath/compopts/compopts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
Package compopts provides CompileOption values for FHIRPath.
This package exists to isolate the options away from the core FHIRPath logic,
since this will simplify discovery of compile-specific options.
*/
package compopts

import (
"errors"

"github.com/verily-src/fhirpath-go/fhirpath/internal/opts"
"github.com/verily-src/fhirpath-go/fhirpath/internal/parser"
)

var ErrMultipleTransforms = errors.New("multiple transforms provided")

// AddFunction creates a CompileOption that will register a custom FHIRPath
// function that can be called during evaluation with the given name.
//
// If the function already exists, then compilation will return an error.
func AddFunction(name string, fn any) opts.CompileOption {
return opts.Transform(func(cfg *opts.CompileConfig) error {
return cfg.Table.Register(name, fn)
})
}

// Transform creates a CompileOption that will set a transform
// to be called on each expression returned by the Visitor.
//
// If there is already a Transform set, then compilation will return an error.
func Transform(v parser.VisitorTransform) opts.CompileOption {
return opts.Transform(func(cfg *opts.CompileConfig) error {
if cfg.Transform != nil {
return ErrMultipleTransforms
}
cfg.Transform = v
return nil
})
}

// Permissive is an option that enables deprecated behavior in FHIRPath field
// navigation. This can be used as a temporary fix for FHIRpaths that have never
// been valid FHIRPaths, but have worked up until this point.
//
// This option is marked Deprecated so that it nags users until the paths can
// be resolved.
//
// Deprecated: Please update FHIRPaths whenever possible.
func Permissive() opts.CompileOption {
return opts.Transform(func(cfg *opts.CompileConfig) error {
cfg.Permissive = true
return nil
})
}
9 changes: 9 additions & 0 deletions fhirpath/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Package fhirpath implements the FHIRPath specification.
More documentation about the FHIRPath specification can be found on HL7
websites:
* https://www.hl7.org/fhir/fhirpath.html
* http://hl7.org/fhirpath/N1/
*/
package fhirpath
74 changes: 74 additions & 0 deletions fhirpath/evalopts/evalopts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
Package evalopts provides EvaluateOption values for FHIRPath.
This package exists to isolate the options away from the core FHIRPath logic,
since this will simplify discovery of evaluation-specific options.
*/
package evalopts

import (
"errors"
"fmt"
"time"

"github.com/verily-src/fhirpath-go/internal/fhir"
"github.com/verily-src/fhirpath-go/fhirpath/internal/opts"
"github.com/verily-src/fhirpath-go/fhirpath/system"
)

var (
ErrUnsupportedType = errors.New("external constant type not supported")
ErrExistingConstant = errors.New("constant already exists")
)

// OverrideTime returns an EvaluateOption that can be used to override the time
// that will be used in FHIRPath expressions.
func OverrideTime(t time.Time) opts.EvaluateOption {
return opts.Transform(func(cfg *opts.EvaluateConfig) error {
cfg.Context.Now = t
return nil
})
}

// EnvVariable returns an EvaluateOption that sets FHIRPath environment variables
// (e.g. %action).
//
// The input must be one of:
// - A FHIRPath System type,
// - A FHIR Element or Resource type, or
// - A FHIRPath Collection, containing the above types.
//
// If an EnvVariable is specified that already exists in the expression, then
// evaluation will yield an ErrExistingConstant error. If an EnvVariable is
// contains a type that is not one of the above valid types, then evaluation
// will yield an ErrUnsupportedType error.
func EnvVariable(name string, value any) opts.EvaluateOption {
return opts.Transform(func(cfg *opts.EvaluateConfig) error {
if err := validateType(value); err != nil {
return err
}
if _, ok := cfg.Context.ExternalConstants[name]; !ok {
cfg.Context.ExternalConstants[name] = value
return nil
}
return fmt.Errorf("%w: %s", ErrExistingConstant, name)
})
}

// validateType validates that the input type is a supported
// fhir proto or System type. If a system.Collection is passed in,
// recursively checks each element.
func validateType(input any) error {
var err error
switch v := input.(type) {
case fhir.Base, system.Any:
break
case system.Collection:
for _, elem := range v {
err = errors.Join(err, validateType(elem))
}
default:
err = fmt.Errorf("%w: %T", ErrUnsupportedType, input)
}
return err
}
118 changes: 118 additions & 0 deletions fhirpath/fhirpath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package fhirpath

import (
"errors"

"github.com/verily-src/fhirpath-go/internal/slices"
"github.com/verily-src/fhirpath-go/internal/fhir"
"github.com/verily-src/fhirpath-go/fhirpath/evalopts"
"github.com/verily-src/fhirpath-go/fhirpath/internal/compile"
"github.com/verily-src/fhirpath-go/fhirpath/internal/expr"
"github.com/verily-src/fhirpath-go/fhirpath/internal/opts"
"github.com/verily-src/fhirpath-go/fhirpath/internal/parser"
"github.com/verily-src/fhirpath-go/fhirpath/system"
)

var (
ErrInvalidField = expr.ErrInvalidField
ErrUnsupportedType = evalopts.ErrUnsupportedType
ErrExistingConstant = evalopts.ErrExistingConstant
)

// Expression is the FHIRPath expression that will be compiled from a FHIRPath string
type Expression struct {
expression expr.Expression
path string
}

// Compile parses and compiles the FHIRPath expression down to a single
// Expression object.
//
// If there are any syntax or semantic errors, this will return an
// error indicating the compilation failure reason.
func Compile(expr string, options ...CompileOption) (*Expression, error) {
config, err := compile.PopulateConfig(options...)
if err != nil {
return nil, err
}

tree, err := compile.Tree(expr)
if err != nil {
return nil, err
}

visitor := &parser.FHIRPathVisitor{
Functions: config.Table,
Permissive: config.Permissive,
}
vr, ok := visitor.Visit(tree).(*parser.VisitResult)
if !ok {
return nil, errors.New("input expression currently unsupported")
}

if vr.Error != nil {
return nil, vr.Error
}
return &Expression{
expression: vr.Result,
path: expr,
}, nil
}

// String returns the string representation of this FHIRPath expression.
// This is just the input that initially produced the FHIRPath value.
func (e *Expression) String() string {
return e.path
}

// MustCompile compiles the FHIRpath expression input, and returns the
// compiled expression. If any compilation error occurs, this function
// will panic.
func MustCompile(expr string, opts ...CompileOption) *Expression {
result, err := Compile(expr, opts...)
if err != nil {
panic(err)
}
return result
}

// Evaluate the expression, returning either a collection of elements, or error
func (e *Expression) Evaluate(input []fhir.Resource, options ...EvaluateOption) (system.Collection, error) {
config := &opts.EvaluateConfig{
Context: expr.InitializeContext(slices.MustConvert[any](input)),
}
config, err := opts.ApplyOptions(config, options...)
if err != nil {
return nil, err
}

collection := slices.MustConvert[any](input)
return e.expression.Evaluate(config.Context, collection)
}

// EvaluateAsString evaluates the expression, returning a string or error
func (e *Expression) EvaluateAsString(input []fhir.Resource, options ...EvaluateOption) (string, error) {
got, err := e.Evaluate(input, options...)
if err != nil {
return "", err
}
return got.ToString()
}

// EvaluateAsBool evaluates the expression, returning either a boolean or error
func (e *Expression) EvaluateAsBool(input []fhir.Resource, options ...EvaluateOption) (bool, error) {
got, err := e.Evaluate(input, options...)
if err != nil {
return false, err
}
return got.ToBool()
}

// EvaluateAsInt32 evaluates the expression, returning either an int32 or error
func (e *Expression) EvaluateAsInt32(input []fhir.Resource, options ...EvaluateOption) (int32, error) {
got, err := e.Evaluate(input, options...)
if err != nil {
return 0, err
}
return got.ToInt32()
}
Loading

0 comments on commit e29f6e5

Please sign in to comment.