From abfee322cd3adf030de22b65b3f31061034edd06 Mon Sep 17 00:00:00 2001 From: Matthew Rodusek <7519129+bitwizeshift@users.noreply.github.com> Date: Sun, 7 Jul 2024 22:46:46 -0400 Subject: [PATCH] Add internal package for evaluate context When evaluating a FHIRPath parse-tree, it's important to be able to provide some level of contextual information to the evaluation process. This commit adds an internal package to the project that provides a context object that can be used to provide this information. --- internal/envcontext/envcontext.go | 78 +++++++++ internal/envcontext/envcontext_test.go | 225 +++++++++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 internal/envcontext/envcontext.go create mode 100644 internal/envcontext/envcontext_test.go diff --git a/internal/envcontext/envcontext.go b/internal/envcontext/envcontext.go new file mode 100644 index 0000000..682af4a --- /dev/null +++ b/internal/envcontext/envcontext.go @@ -0,0 +1,78 @@ +/* +Package envcontext provides definitions for Context objects that document +environment variables, as defined in FHIRPath. +*/ +package envcontext + +import "context" + +type exprKey struct{} +type exprEntries map[string]any + +// Lookup retrieves a value from the context by name. +func Lookup(ctx context.Context, name string) (any, bool) { + if ctx == nil { + return nil, false + } + value := ctx.Value(exprKey{}) + if value == nil { + return nil, false + } + + // This case should never logically occur, but is added as a precaution. + entries := value.(exprEntries) + if entries == nil { + return nil, false + } + result, ok := entries[name] + return result, ok +} + +// Get retrieves a value from the context by name. +func Get(ctx context.Context, name string) any { + value, _ := Lookup(ctx, name) + return value +} + +// GetOr retrieves a value from the context by name, or returns a default value. +func GetOr(ctx context.Context, name string, def any) any { + value, ok := Lookup(ctx, name) + if !ok { + return def + } + return value +} + +// WithEntry adds a single environment value by name to the context. +func WithEntry(ctx context.Context, name string, value any) context.Context { + if ctx == nil { + ctx = context.Background() + } + exprCtx := ctx.Value(exprKey{}) + if exprCtx == nil { + exprCtx = exprEntries{} + } + entries := exprCtx.(exprEntries) + entries[name] = value + return context.WithValue(ctx, exprKey{}, entries) +} + +// WithEntries adds multiple environment values by name to the context. +func WithEntries(ctx context.Context, values map[string]any) context.Context { + if ctx == nil { + ctx = context.Background() + } + exprCtx := ctx.Value(exprKey{}) + if exprCtx == nil { + exprCtx = exprEntries{} + } + entries := exprCtx.(exprEntries) + if entries == nil { + entries = exprEntries{} + } + + for key, value := range values { + entries[key] = value + } + return context.WithValue(ctx, exprKey{}, entries) +} diff --git a/internal/envcontext/envcontext_test.go b/internal/envcontext/envcontext_test.go new file mode 100644 index 0000000..96df4f3 --- /dev/null +++ b/internal/envcontext/envcontext_test.go @@ -0,0 +1,225 @@ +package envcontext_test + +import ( + "context" + "testing" + + "github.com/friendly-fhir/go-fhirpath/internal/envcontext" +) + +func TestLookup(t *testing.T) { + testCases := []struct { + name string + ctx context.Context + key string + want any + wantOK bool + }{ + { + name: "Nil context returns nil", + ctx: nil, + key: "key", + want: nil, + wantOK: false, + }, { + name: "Empty context returns nil", + ctx: context.Background(), + key: "key", + want: nil, + wantOK: false, + }, { + name: "Context with no entries returns nil", + ctx: envcontext.WithEntries(context.Background(), nil), + key: "key", + want: nil, + wantOK: false, + }, { + name: "Context with entry returns value", + ctx: envcontext.WithEntry(context.Background(), "key", "value"), + key: "key", + want: "value", + wantOK: true, + }, { + name: "Context with non-matching entries returns nil", + ctx: envcontext.WithEntry(context.Background(), "other-key", "value"), + key: "key", + want: nil, + wantOK: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, ok := envcontext.Lookup(tc.ctx, tc.key) + + if got, want := ok, tc.wantOK; got != want { + t.Fatalf("Lookup() ok = %v; want %v", got, want) + } + + if got, want := got, tc.want; got != want { + t.Errorf("Lookup() = %v; want %v", got, want) + } + }) + } +} + +func TestGet(t *testing.T) { + testCases := []struct { + name string + ctx context.Context + key string + want any + }{ + { + name: "Nil context returns nil", + ctx: nil, + key: "key", + want: nil, + }, { + name: "Empty context returns nil", + ctx: context.Background(), + key: "key", + want: nil, + }, { + name: "Context with entry returns value", + ctx: envcontext.WithEntry(context.Background(), "key", "value"), + key: "key", + want: "value", + }, { + name: "Context with non-matching entries returns nil", + ctx: envcontext.WithEntry(context.Background(), "other-key", "value"), + key: "key", + want: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := envcontext.Get(tc.ctx, tc.key) + + if got, want := got, tc.want; got != want { + t.Errorf("Get() = %v; want %v", got, want) + } + }) + } +} + +func TestGetOr(t *testing.T) { + testCases := []struct { + name string + ctx context.Context + key string + def any + want any + }{ + { + name: "Nil context returns default", + ctx: nil, + key: "key", + def: "default", + want: "default", + }, { + name: "Empty context returns default", + ctx: context.Background(), + key: "key", + def: "default", + want: "default", + }, { + name: "Context with entry returns value", + ctx: envcontext.WithEntry(context.Background(), "key", "value"), + key: "key", + def: "default", + want: "value", + }, { + name: "Context with non-matching entries returns default", + ctx: envcontext.WithEntry(context.Background(), "other-key", "value"), + key: "key", + def: "default", + want: "default", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := envcontext.GetOr(tc.ctx, tc.key, tc.def) + + if got, want := got, tc.want; got != want { + t.Errorf("GetOr() = %v; want %v", got, want) + } + }) + } +} + +func TestWithEntry(t *testing.T) { + testCases := []struct { + name string + ctx context.Context + key string + val any + }{ + { + name: "Nil context adds entry", + ctx: nil, + key: "key", + val: "value", + }, { + name: "Empty context adds entry", + ctx: context.Background(), + key: "key", + val: "value", + }, { + name: "Context with entry adds entry", + ctx: envcontext.WithEntry(context.Background(), "other-key", "value"), + key: "key", + val: "value", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := envcontext.WithEntry(tc.ctx, tc.key, tc.val) + + got := envcontext.Get(ctx, tc.key) + + if got, want := got, tc.val; got != want { + t.Errorf("WithEntry() = %v; want %v", got, want) + } + }) + } +} + +func TestWithEntries(t *testing.T) { + testCases := []struct { + name string + ctx context.Context + values map[string]any + }{ + { + name: "Nil context adds entries", + ctx: nil, + values: map[string]any{"key": "value"}, + }, { + name: "Empty context adds entries", + ctx: context.Background(), + values: map[string]any{"key": "value"}, + }, { + name: "Context with entry adds entries", + ctx: envcontext.WithEntry(context.Background(), "other-key", "value"), + values: map[string]any{"key": "value"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := envcontext.WithEntries(tc.ctx, tc.values) + + for key, val := range tc.values { + got := envcontext.Get(ctx, key) + + if got, want := got, val; got != want { + t.Errorf("WithEntries() = %v; want %v", got, want) + } + } + }) + } +}