Skip to content

Commit

Permalink
Add reflect package
Browse files Browse the repository at this point in the history
FHIRPath defines a "reflection" specification to allow for getting info
on the type of an object, and to allow for dynamic access to fields and
methods. This commit adds the reflect package to the FHIRPath library
which implements part of this functionality.

Additionally, to make it traversable, a `Reflect` Namespace object has
been included as well.
  • Loading branch information
bitwizeshift committed Jul 10, 2024
1 parent be92a19 commit 232d5c3
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 26 deletions.
55 changes: 32 additions & 23 deletions namespace/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package namespace

import (
"fmt"
"reflect"
stdreflect "reflect"

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

Expand All @@ -13,12 +14,15 @@ var (
R4 = New("FHIR", fhirNamer, r4Element, r4Resource, r4Domain, r4Backbone)

// System is the system namespace that contains all system types.
System = New("System", systemNamer, systemAny)
System = New("System", basicNamer, systemAny)

// Reflect is the namespace that contains all reflection types.
Reflect = New("Reflect", basicNamer, reflectInfo, reflectElement)
)

// Select returns the namespace that contains the specified type. If no
// namespace contains the type, then nil is returned.
func Select(t reflect.Type, namespaces ...*Namespace) *Namespace {
func Select(t stdreflect.Type, namespaces ...*Namespace) *Namespace {
for _, ns := range namespaces {
if ns.Contains(t) {
return ns
Expand All @@ -30,15 +34,15 @@ func Select(t reflect.Type, namespaces ...*Namespace) *Namespace {
// Namer is an interface that can be used to generate names for types.
type Namer interface {
// Name returns the name of the type.
Name(reflect.Type) string
Name(stdreflect.Type) reflect.TypeSpecifier
}

// NamerFunc is a convenience type that implements the Namer interface. This
// simplifies constructing a function that acts as a name function.
type NamerFunc func(reflect.Type) string
type NamerFunc func(stdreflect.Type) reflect.TypeSpecifier

// Name calls the underlying function to generate the name.
func (f NamerFunc) Name(t reflect.Type) string {
func (f NamerFunc) Name(t stdreflect.Type) reflect.TypeSpecifier {
return f(t)
}

Expand All @@ -50,11 +54,11 @@ var _ Namer = (*NamerFunc)(nil)
type Namespace struct {
namer Namer
name string
interfaces []reflect.Type
interfaces []stdreflect.Type
}

// New constructs a new namespace with the specified name, namer, and interfaces.
func New(name string, namer Namer, ifaces ...reflect.Type) *Namespace {
func New(name string, namer Namer, ifaces ...stdreflect.Type) *Namespace {
return &Namespace{
name: name,
namer: namer,
Expand All @@ -69,18 +73,18 @@ func (n *Namespace) String() string {

// QualifiedName returns the qualified name of the type in this namespace.
// This is a namespace-prefixed type-name.
func (n *Namespace) QualifiedName(t reflect.Type) string {
return fmt.Sprintf("%s.%s", n.name, n.namer.Name(t))
func (n *Namespace) QualifiedName(t stdreflect.Type) reflect.TypeSpecifier {
return reflect.TypeSpecifier(fmt.Sprintf("%s.%s", n.name, n.namer.Name(t)))
}

// Name returns the name of the type as it appears in the namespace. This will
// not include the namespace prefix.
func (n *Namespace) Name(t reflect.Type) string {
func (n *Namespace) Name(t stdreflect.Type) reflect.TypeSpecifier {
return n.namer.Name(t)
}

// Contains returns true if the namespace contains the specified type.
func (n *Namespace) Contains(t reflect.Type) bool {
func (n *Namespace) Contains(t stdreflect.Type) bool {
for _, iface := range n.interfaces {
if t.Implements(iface) {
return true
Expand All @@ -90,21 +94,26 @@ func (n *Namespace) Contains(t reflect.Type) bool {
}

var (
systemAny = reflect.TypeOf((*system.Any)(nil)).Elem()
r4Element = reflect.TypeOf((*fhir.Element)(nil)).Elem()
r4Backbone = reflect.TypeOf((*fhir.BackboneElement)(nil)).Elem()
r4Resource = reflect.TypeOf((*fhir.Resource)(nil)).Elem()
r4Domain = reflect.TypeOf((*fhir.DomainResource)(nil)).Elem()

fhirNamer = NamerFunc(func(t reflect.Type) string {
if t.Kind() == reflect.Ptr {
systemAny = stdreflect.TypeOf((*system.Any)(nil)).Elem()
r4Element = stdreflect.TypeOf((*fhir.Element)(nil)).Elem()
r4Backbone = stdreflect.TypeOf((*fhir.BackboneElement)(nil)).Elem()
r4Resource = stdreflect.TypeOf((*fhir.Resource)(nil)).Elem()
r4Domain = stdreflect.TypeOf((*fhir.DomainResource)(nil)).Elem()
reflectInfo = stdreflect.TypeOf((*reflect.Info)(nil)).Elem()
reflectElement = stdreflect.TypeOf((*reflect.InfoElement)(nil)).Elem()

fhirNamer = NamerFunc(func(t stdreflect.Type) reflect.TypeSpecifier {
if t.Kind() == stdreflect.Ptr {
t = t.Elem()
}
name := t.Name()
// TODO: primitives should be in camelCase instead of PascalCase.
return name
return reflect.TypeSpecifier(name)
})
systemNamer = NamerFunc(func(t reflect.Type) string {
return t.Name()
basicNamer = NamerFunc(func(t stdreflect.Type) reflect.TypeSpecifier {
if t.Kind() == stdreflect.Ptr {
t = t.Elem()
}
return reflect.TypeSpecifier(t.Name())
})
)
35 changes: 32 additions & 3 deletions namespace/namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
fhir "github.com/friendly-fhir/go-fhir/r4/core"
"github.com/friendly-fhir/go-fhir/r4/core/resources/patient"
"github.com/friendly-fhir/go-fhirpath/namespace"
fpreflect "github.com/friendly-fhir/go-fhirpath/reflect"
"github.com/friendly-fhir/go-fhirpath/system"
)

Expand Down Expand Up @@ -36,12 +37,16 @@ func TestSelect(t *testing.T) {
name: "Unknown type",
input: reflect.TypeOf((*testing.T)(nil)),
want: nil,
}, {
name: "Reflect type",
input: reflect.TypeOf((*fpreflect.ClassInfo)(nil)),
want: namespace.Reflect,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := namespace.Select(tc.input, namespace.R4, namespace.System)
got := namespace.Select(tc.input, namespace.R4, namespace.System, namespace.Reflect)

if got != tc.want {
t.Errorf("namespace.Select() = %v; want %v", got, tc.want)
Expand All @@ -55,7 +60,7 @@ func TestNamespaceName(t *testing.T) {
name string
input reflect.Type
namespace *namespace.Namespace
want string
want fpreflect.TypeSpecifier
}{
{
name: "FHIR resource",
Expand All @@ -77,6 +82,11 @@ func TestNamespaceName(t *testing.T) {
input: reflect.TypeOf((*system.String)(nil)).Elem(),
namespace: namespace.System,
want: "String",
}, {
name: "Reflect type",
input: reflect.TypeOf((*fpreflect.ClassInfo)(nil)),
namespace: namespace.Reflect,
want: "ClassInfo",
},
}

Expand Down Expand Up @@ -105,6 +115,10 @@ func TestNamespaceString(t *testing.T) {
name: "System namespace",
namespace: namespace.System,
want: "System",
}, {
name: "Reflect namespace",
namespace: namespace.Reflect,
want: "Reflect",
},
}

Expand All @@ -124,7 +138,7 @@ func TestNamespaceQualifiedName(t *testing.T) {
name string
input reflect.Type
namespace *namespace.Namespace
want string
want fpreflect.TypeSpecifier
}{
{
name: "FHIR resource",
Expand All @@ -146,6 +160,11 @@ func TestNamespaceQualifiedName(t *testing.T) {
input: reflect.TypeOf((*system.String)(nil)).Elem(),
namespace: namespace.System,
want: "System.String",
}, {
name: "Reflect type",
input: reflect.TypeOf((*fpreflect.ClassInfo)(nil)),
namespace: namespace.Reflect,
want: "Reflect.ClassInfo",
},
}

Expand Down Expand Up @@ -192,6 +211,16 @@ func TestNamespaceContains(t *testing.T) {
input: reflect.TypeOf((*system.String)(nil)).Elem(),
namespace: namespace.R4,
want: false,
}, {
name: "Reflect type in Reflect namespace",
input: reflect.TypeOf((*fpreflect.ClassInfo)(nil)),
namespace: namespace.Reflect,
want: true,
}, {
name: "FHIR resource in Reflect namespace",
input: reflect.TypeOf((*patient.Patient)(nil)),
namespace: namespace.Reflect,
want: false,
},
}

Expand Down
114 changes: 114 additions & 0 deletions reflect/reflect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
Package reflect implements §10.2. Reflection from the FHIRPath specification.
The types defined in this package are a subset of the types defined in the
Clinical Query Language (CQL) specification, but are limited solely to the usage
within FHIRPath.
See https://hl7.org/fhirpath/N1/#reflection for more information.
*/
package reflect

// TypeSpecifier represents a reflection of a FHIRPath type specifier.
//
// Type specifier may name either FHIR or System-namespaced types, and may be
// either scalar or list values. In most FHIRPath applications, these will only
// ever be seen as scalar type values corresponding to the underlying resource or
// element. Lists are a byproduct that only present itself during reflection
// operations, but otherwise should never be seen in expressions.
type TypeSpecifier string

// Info represents a reflection of a FHIRPath type.
type Info interface {
isInfo()
}

// InfoElement represents an element from a reflection of a FHIRPath type.
type InfoElement interface {
isInfoElement()
}

// SimpleTypeInfo is a Go implementation of §10.2.1 "Primitive Types" from
// the FHIRPath specification.
//
// See: https://hl7.org/fhirpath/N1/#primitive-types
type SimpleTypeInfo struct {
Namespace string `json:"namespace" yaml:"namespace" fhirpath:"namespace"`
Name string `json:"name" yaml:"name" fhirpath:"name"`
BaseType TypeSpecifier `json:"baseType" yaml:"baseType" fhirpath:"baseType"`
}

func (SimpleTypeInfo) isInfo() {}

var _ Info = (*SimpleTypeInfo)(nil)

// ClassInfoElement is a Go implementation of §10.2.2 "Class Types" from
// the FHIRPath specification.
//
// This is a compound type that is built into larger structures in the
// [ClassInfo] type.
//
// See https://hl7.org/fhirpath/N1/#class-types
type ClassInfoElement struct {
Name string `json:"name" yaml:"name" fhirpath:"name"`
Type TypeSpecifier `json:"type" yaml:"type" fhirpath:"type"`
IsOneBased bool `json:"isOneBased" yaml:"isOneBased" fhirpath:"isOneBased"`
}

func (ClassInfoElement) isInfoElement() {}

// ClassInfo is a Go implementation of §10.2.2 "Class Types" from
// the FHIRPath specification.
//
// See https://hl7.org/fhirpath/N1/#class-types
type ClassInfo struct {
Namespace string `json:"namespace" yaml:"namespace" fhirpath:"namespace"`
Name string `json:"name" yaml:"name" fhirpath:"name"`
BaseType TypeSpecifier `json:"baseType" yaml:"baseType" fhirpath:"baseType"`
Element []ClassInfoElement `json:"element" yaml:"element" fhirpath:"element"`
}

func (ClassInfo) isInfo() {}

var _ Info = (*ClassInfo)(nil)

// ListTypeInfo is a Go implementation of §10.2.3 "Collection Types" from
// the FHIRPath specification.
//
// See https://hl7.org/fhirpath/N1/#collection-types
type ListTypeInfo struct {
ElementType TypeSpecifier `json:"elementType" yaml:"elementType" fhirpath:"elementType"`
}

func (ListTypeInfo) isInfo() {}

var _ Info = (*ListTypeInfo)(nil)

// TupleTypeInfoElement is a Go implementation of §10.2.4 "Anonymous Types" from
// the FHIRPath specification.
//
// This is a compound type that is built into larger structures in the
// [TupleTypeInfo] type.
//
// See https://hl7.org/fhirpath/N1/#anonymous-types
type TupleTypeInfoElement struct {
Name string `json:"name" yaml:"name" fhirpath:"name"`
Type TypeSpecifier `json:"type" yaml:"type" fhirpath:"type"`
IsOneBased bool `json:"isOneBased" yaml:"isOneBased" fhirpath:"isOneBased"`
}

func (TupleTypeInfoElement) isInfoElement() {}

var _ InfoElement = (*TupleTypeInfoElement)(nil)

// TupleTypeInfo is a Go implementation of §10.2.4 "Anonymous Types" from
// the FHIRPath specification.
//
// See https://hl7.org/fhirpath/N1/#anonymous-types
type TupleTypeInfo struct {
Element []TupleTypeInfoElement `json:"element" yaml:"element" fhirpath:"element"`
}

func (TupleTypeInfo) isInfo() {}

var _ Info = (*TupleTypeInfo)(nil)

0 comments on commit 232d5c3

Please sign in to comment.