Skip to content

Commit

Permalink
Add initial system package
Browse files Browse the repository at this point in the history
This adds an initial system package for the FHIRPath implementation.
The types are from the N1 specification, and are beginning with an
initial set of types:

* Booleans
* Integers
* Strings

This is the first step in the implementation of the FHIRPath. This
format is not expected to be the final format, since the `fhir` package
is still itself unstable.
  • Loading branch information
bitwizeshift committed Jul 8, 2024
1 parent e048960 commit 97b2a25
Show file tree
Hide file tree
Showing 13 changed files with 1,004 additions and 1 deletion.
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ go 1.22.3

require github.com/antlr4-go/antlr/v4 v4.13.1

require golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
require github.com/google/go-cmp v0.6.0 // indirect

require (
github.com/friendly-fhir/go-fhir v0.0.0-20240627230005-9ef2174c1f29
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/friendly-fhir/go-fhir v0.0.0-20240627035249-eacfb3386af5 h1:D6ephfRcx17SrIHrWz2HTDu19Y6Zy1J9YTRyal0ZUnw=
github.com/friendly-fhir/go-fhir v0.0.0-20240627035249-eacfb3386af5/go.mod h1:dHmN8TwYULp9TAOI1D0cR1RpsIntTM75Y/aUq3v9fY0=
github.com/friendly-fhir/go-fhir v0.0.0-20240627230005-9ef2174c1f29 h1:8PXLQ1rNBD7CRPlDjJYaDFu3ZQFSsSnV5bl+3QUSs0k=
github.com/friendly-fhir/go-fhir v0.0.0-20240627230005-9ef2174c1f29/go.mod h1:dHmN8TwYULp9TAOI1D0cR1RpsIntTM75Y/aUq3v9fY0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
38 changes: 38 additions & 0 deletions internal/esc/esc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Package esc provides basic functionality for handling the ESC (escape) sequences
in the FHIRPath grammar.
*/
package esc

import (
"fmt"
"strconv"
"strings"
)

var escaper *strings.Replacer

func init() {
escaper = strings.NewReplacer(
`\'`, `\u0027`,
`\"`, `\u0022`,
"\\`", `\u0060`,
"\\r", `\u000d`,
"\\n", `\u000a`,
"\\t", `\u0009`,
"\\f", `\u000c`,
"\\\\", `\u005c`,
)
}

func Parse(input string) (string, error) {
result := input
result = escaper.Replace(result)
// Re-escape any remaining quotes, so that Unquote won't fail.
result = strings.ReplaceAll(result, `"`, `\u0022`)
result, err := strconv.Unquote(fmt.Sprintf(`"%v"`, result))
if err != nil {
return "", err
}
return result, nil
}
17 changes: 17 additions & 0 deletions system/any.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package system

// Any is the top-level system type for FHIRPath.
type Any interface {
isAny()
}

// IsType checks whether a given type string is a valid FHIRPath System type
// name value. This function is case-sensitive.
func IsType(ty string) bool {
switch ty {
case "Boolean", "Integer", "Any", "Date", "DateTime", "Decimal", "Quantity",
"String", "Time":
return true
}
return false
}
121 changes: 121 additions & 0 deletions system/bool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package system

import (
"encoding"
"encoding/json"
"fmt"
"strconv"

fhir "github.com/friendly-fhir/go-fhir/r4/core"
)

// Boolean is the FHIRPath system-type representation of the "boolean" value.
type Boolean bool

// NewBoolean constructs a Boolean object with the underlying value.
//
// This function primarily exists for symmetry with the other constructor
// functions.
func NewBoolean(b bool) Boolean {
return Boolean(b)
}

// ParseBoolean parses a string into the valid FHIRPath System.Boolean type.
func ParseBoolean(str string) (Boolean, error) {
switch str {
case "true":
return true, nil
case "false":
return false, nil
}
return false, newParseError[Boolean](str, nil)
}

// MustParseBoolean parses a boolean string, and panics if the value is invalid.
func MustParseBoolean(str string) Boolean {
got, err := ParseBoolean(str)
if err != nil {
panic(err)
}
return got
}

func (Boolean) isAny() {}

// Negate returns the inverse polarity of this boolean value.
func (b Boolean) Negate() Boolean {
return !b
}

// Bool returns the Go boolean representation of the System.Boolean.
func (b Boolean) Bool() bool {
return bool(b)
}

// Formatting

// String returns the string representation of the System.Boolean.
func (b Boolean) String() string {
return strconv.FormatBool(bool(b))
}

// Format implements the fmt.Formatter interface.
func (b Boolean) Format(s fmt.State, verb rune) {
fmt.Fprintf(s, "%"+string(verb), bool(b))
}

var (
_ fmt.Stringer = (*Boolean)(nil)
_ fmt.Formatter = (*Boolean)(nil)
)

// R4 conversions

// FromR4 converts a FHIR Boolean type into a System.Boolean type.
func (b *Boolean) FromR4(r *fhir.Boolean) {
*b = Boolean(r.Value)
}

// R4 converts this System.Boolean into a FHIR Boolean type.
func (b Boolean) R4() *fhir.Boolean {
return &fhir.Boolean{Value: bool(b)}
}

// JSON conversions

// MarshalJSON converts this Boolean object into a JSON object.
func (b Boolean) MarshalJSON() ([]byte, error) {
return json.Marshal(bool(b))
}

// UnmarshalJSON converts a JSON object into a Boolean object.
func (b *Boolean) UnmarshalJSON(data []byte) error {
var value bool
if err := json.Unmarshal(data, &value); err != nil {
return err
}
*b = Boolean(value)
return nil
}

var (
_ json.Marshaler = (*Boolean)(nil)
_ json.Unmarshaler = (*Boolean)(nil)
)

// Text conversions

// MarshalText converts this Boolean object into a text object.
func (b Boolean) MarshalText() ([]byte, error) {
return b.MarshalJSON()
}

// UnmarshalText converts a text object into a Boolean object.
func (b *Boolean) UnmarshalText(text []byte) error {
return b.UnmarshalJSON(text)
}

var (
_ encoding.TextMarshaler = (*Boolean)(nil)
_ encoding.TextUnmarshaler = (*Boolean)(nil)
)
151 changes: 151 additions & 0 deletions system/bool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package system_test

import (
"errors"
"testing"

fhir "github.com/friendly-fhir/go-fhir/r4/core"
"github.com/friendly-fhir/go-fhirpath/system"
"github.com/google/go-cmp/cmp"
)

func TestParseBoolean(t *testing.T) {
testCases := []struct {
input string
want system.Boolean
}{
{"false", false},
{"true", true},
}

for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
got, err := system.ParseBoolean(tc.input)
if err != nil {
t.Fatalf("ParseBoolean() = %v; want nil", err)
}

if got, want := got, tc.want; got != want {
t.Errorf("ParseBoolean() = %v; want %v", got, want)
}

})
}
}

func TestParseBoolean_InvalidString_ReturnsParseError(t *testing.T) {
testCases := []struct {
input string
}{
{"bad value"},
{"b"},
{"TRUE"},
{"1"},
{"FALSE"},
{"0"},
{"False"},
{"True"},
}

for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
_, err := system.ParseBoolean(tc.input)

var parseErr *system.ParseError
ok := errors.As(err, &parseErr)

if got, want := ok, true; got != want {
t.Errorf("ParseBoolean() = %v; want %v", got, want)
}
})
}
}

func TestMustParseBoolean(t *testing.T) {
testCases := []struct {
input string
want system.Boolean
}{
{"false", false},
{"true", true},
}

for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
got := system.MustParseBoolean(tc.input)

if got, want := got, tc.want; got != want {
t.Errorf("ParseBoolean() = %v; want %v", got, want)
}
})
}
}

func TestMustParseBoolean_InvalidString_Panics(t *testing.T) {
testCases := []struct {
input string
}{
{"bad value"},
{"b"},
{"TRUE"},
{"1"},
{"FALSE"},
{"0"},
{"False"},
{"True"},
}

for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
defer func() { _ = recover() }()

system.MustParseBoolean(tc.input)

t.Errorf("MustParseBoolean() = want panic")
})
}
}

func TestBooleanBool(t *testing.T) {
testCases := []struct {
input string
want bool
}{
{"false", false},
{"true", true},
}

for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
v := system.MustParseBoolean(tc.input)

got := v.Bool()

if got, want := got, tc.want; got != want {
t.Errorf("Boolean.Bool() = %v; want %v", got, want)
}
})
}
}

func TestBooleanR4(t *testing.T) {
testCases := []struct {
input string
want *fhir.Boolean
}{
{"false", &fhir.Boolean{Value: false}},
{"true", &fhir.Boolean{Value: true}},
}

for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
v := system.MustParseBoolean(tc.input)

got := v.R4()

if diff := cmp.Diff(got, tc.want); diff != "" {
t.Errorf("Boolean.ToBoolean() mismatch (-got +want):\n%s", diff)
}
})
}
}
41 changes: 41 additions & 0 deletions system/conv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package system

import (
"fmt"

fhir "github.com/friendly-fhir/go-fhir/r4/core"
profile "github.com/friendly-fhir/go-fhir/r4/core/profiles"
)

// FromR4 return a system type from a (valid) FHIR R4 element.
func FromR4(element fhir.Element) (Any, error) {
switch e := element.(type) {
case *fhir.Boolean:
var b Boolean
b.FromR4(e)
return b, nil
case profile.Integer:
var i Integer
i.FromR4(e)
return i, nil
case profile.String:
var s String
s.FromR4(e)
return s, nil
}
return nil, fmt.Errorf("%w: %T is not a valid R4 type", ErrNotConvertible, element)
}

// Normalizes a FHIR R4 type into a system type, if able -- or just returns
// the input value if it's not a FHIR R4 type.
func Normalize(v any) any {
element, ok := v.(fhir.Element)
if !ok {
return v
}
got, err := FromR4(element)
if err != nil {
return element
}
return got
}
Loading

0 comments on commit 97b2a25

Please sign in to comment.