Repository: lyraproj/hiera
Pull request #40 (2019-10-16 11:15:34)

Code samples
This PR contains a set of commits, each focused on some examples that cover some specific area. The details are in the commit comments.


Patch:
diff --git a/cli/hiera.go b/cli/hiera.go
index 335b4e3..3fe6cdb 100644
--- a/cli/hiera.go
+++ b/cli/hiera.go
@@ -106,7 +106,7 @@ func initialize(_ *cobra.Command, _ []string) {
 func cmdLookup(cmd *cobra.Command, args []string) error {
 	cmdOpts.Default = dflt.StringPointer()
 	configOptions := map[string]px.Value{
-		provider.LookupProvidersKey: types.WrapRuntime([]hieraapi.LookupKey{provider.ConfigLookupKey, provider.Environment})}
+		provider.LookupKeyFunctions: types.WrapRuntime([]hieraapi.LookupKey{provider.ConfigLookupKey, provider.Environment})}
 
 	if config != `` {
 		configOptions[hieraapi.HieraConfig] = types.WrapString(config)
diff --git a/hieraapi/deepmerge.go b/hieraapi/deepmerge.go
new file mode 100644
index 0000000..194a9bd
--- /dev/null
+++ b/hieraapi/deepmerge.go
@@ -0,0 +1,13 @@
+package hieraapi
+
+import "github.com/lyraproj/pcore/px"
+
+// DeepMerge will merge the values 'a' and 'b' if both values are hashes or both values are
+// arrays. When this is not the case, no merge takes place and the 'a' argument is returned.
+// The second bool return value true if a merge took place and false when the first argument
+// is returned.
+//
+// When both values are hashes, DeepMerge is called recursively entries with identical keys.
+// When both values are arrays, the merge creates a union of the unique elements from the two arrays.
+// No recursive merge takes place for the array elements.
+var DeepMerge func(a, b px.Value, options map[string]px.Value) (px.Value, bool)
diff --git a/hieraserver/rest/rest.go b/hieraserver/rest/rest.go
index 3e3128e..14824c8 100644
--- a/hieraserver/rest/rest.go
+++ b/hieraserver/rest/rest.go
@@ -62,7 +62,7 @@ var keyPattern = regexp.MustCompile(`^/lookup/(.*)$`)
 
 func startServer(cmd *cobra.Command, _ []string) {
 	configOptions := map[string]px.Value{
-		provider.LookupProvidersKey: types.WrapRuntime([]hieraapi.LookupKey{provider.ConfigLookupKey, provider.Environment})}
+		provider.LookupKeyFunctions: types.WrapRuntime([]hieraapi.LookupKey{provider.ConfigLookupKey, provider.Environment})}
 
 	configOptions[hieraapi.HieraConfig] = types.WrapString(config)
 
diff --git a/internal/init.go b/internal/init.go
index 8077f15..58b8e91 100644
--- a/internal/init.go
+++ b/internal/init.go
@@ -56,6 +56,8 @@ func init() {
 	hieraapi.NotFound = px.Error(hieraapi.KeyNotFound, issue.NoArgs)
 
 	hieraapi.NewKey = newKey
+
+	hieraapi.DeepMerge = DeepMerge
 }
 
 func Lookup(ic hieraapi.Invocation, name string, dflt px.Value, options map[string]px.Value) px.Value {
diff --git a/provider/muxlookupkey.go b/provider/muxlookupkey.go
index eabb2bf..bef6300 100644
--- a/provider/muxlookupkey.go
+++ b/provider/muxlookupkey.go
@@ -6,7 +6,7 @@ import (
 	"github.com/lyraproj/pcore/types"
 )
 
-const LookupProvidersKey = `hiera::lookup::providers`
+const LookupKeyFunctions = `hiera::lookup::providers`
 
 // MuxLookup performs a lookup using all LookupKey function slice registered under the LookupProviderKey key
 // in the given options map. The lookups are performed in the order the functions appear in the
@@ -15,7 +15,7 @@ const LookupProvidersKey = `hiera::lookup::providers`
 // The intended use for this function is when a very simplistic way of configuring Hiera is desired that
 // requires no configuration files.
 func MuxLookupKey(c hieraapi.ServerContext, key string) px.Value {
-	if pv := c.Option(LookupProvidersKey); pv != nil {
+	if pv := c.Option(LookupKeyFunctions); pv != nil {
 		if rpv, ok := pv.(*types.RuntimeValue); ok {
 			var pvs []hieraapi.LookupKey
 			if pvs, ok = rpv.Interface().([]hieraapi.LookupKey); ok {


Test patch:
diff --git a/examples_test/config_test.go b/examples_test/config_test.go
new file mode 100644
index 0000000..b31eeee
--- /dev/null
+++ b/examples_test/config_test.go
@@ -0,0 +1,148 @@
+package examples_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/lyraproj/hiera/hiera"
+	"github.com/lyraproj/hiera/hieraapi"
+	"github.com/lyraproj/hiera/provider"
+	"github.com/lyraproj/pcore/px"
+	"github.com/lyraproj/pcore/types"
+)
+
+// sayHello is a very simple "lookup_key" function that just returns the result of concatenating
+// the key with the string " world".
+func sayHello(pc hieraapi.ServerContext, key string) px.Value {
+	return types.WrapString(key + ` world`)
+}
+
+/*
+ Hiera will always use a single "lookup_key" function at the very top, henceforth referred to as the "top level
+ function". This function determines the hierarchy (or lack thereof) that Hiera uses. This file explains the three such
+ functions: the sayHello example function, the MuxLookupKey which aggregates other lookup_key functions, and the
+ ConfigLookupKey which sets up a hierarchy defined in a yaml configuration.
+*/
+
+// TestConfig_hardwired utilizes Hiera in the simplest way possible. No configuration file and no options. Just
+// a function performing a lookup of a key. In other words, this single function is the entire hierarchy.
+func TestConfig_hardwired(t *testing.T) {
+	// Use the hiera.DoWithParent to initialize a Hiera context (session) with the sayHello as the top-level function and
+	// perform a lookup.
+	//
+	// The DoWithParent is meant to be called once and the created context can then be used for any number of lookups that
+	// uses the same configuration. The session's life-cycle can be compared to the compiler's life-cycle in puppet.
+	hiera.DoWithParent(context.Background(), sayHello, nil, func(c px.Context) {
+		result := hiera.Lookup(hiera.NewInvocation(c, nil, nil), `hello`, nil, nil)
+		if result == nil || `hello world` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+	})
+}
+
+// TestHelloWorld_semiHardWired uses the "lookup_key" function MuxLookupKey. This function use the configuration
+// option LookupKeyFunctions where it expects to find a slice of "lookup_key" functions to use. Those functions form
+// top level hierarchy that is configurable from code. Very useful if you for instance want to introduce different
+// lookup layers such as "global", "environment", and "module" or in other ways build a complex lookup hierarchy that
+// service that goes beyond what can be defined in the yaml configuration.
+func TestConfig_semiHardWired(t *testing.T) {
+	// Create options valid for this Hiera session.
+	configOptions := make(map[string]px.Value)
+
+	// The LookupProvidersKey stores a go slice of Hiera "lookup_key" functions that serve as the top level functions.
+	configOptions[provider.LookupKeyFunctions] = types.WrapRuntime([]hieraapi.LookupKey{sayHello})
+
+	// Initialize a Hiera session with the MuxLookupKey as the top-level function and perform a lookup and
+	// the created configOptions.
+	hiera.DoWithParent(context.Background(), provider.MuxLookupKey, configOptions, func(c px.Context) {
+		result := hiera.Lookup(hiera.NewInvocation(c, nil, nil), `hello`, nil, nil)
+		if result == nil || `hello world` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+	})
+}
+
+/*
+ The remaining tests in this file use the ConfigLookupKey provider. This provider will consult the configuration
+ options HieraRoot, HieraConfigFileName, and HieraConfig to determine the path of the configuration file. Use of
+ HieraConfig is mutually exclusive with HieraRoot and HieraConfigFileName.
+
+ HieraRoot will default to the current working directory.
+
+ HieraConfigFileName will default to "hiera.yaml".
+
+ If the HieraRoot or HieraConfig are relative paths, they will be considered relative to the current directory.
+
+ The HieraConfigFileName must be relative to the HieraRoot.
+*/
+
+// TestHelloWorld_yamlConfig uses the "lookup_key" function ConfigLookupKey and HieraRoot. The ConfigLookupKey is
+// the most commonly used top-level function in Hiera. It finds a yaml configuration on disk and then configures
+// everything according to the hierarchy specified in that file.
+func TestHelloWorld_yamlConfig(t *testing.T) {
+	configOptions := make(map[string]px.Value)
+	configOptions[hieraapi.HieraRoot] = types.WrapString(`testdata`)
+
+	// Initialize a Hiera session with the ConfigLookupKey as the top-level function configured using the configOptions.
+	hiera.DoWithParent(context.Background(), provider.ConfigLookupKey, configOptions, func(c px.Context) {
+		result := hiera.Lookup(hiera.NewInvocation(c, nil, nil), `hello`, nil, nil)
+		if result == nil || `yaml data says hello` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+	})
+}
+
+// TestHelloWorld_explicitYamlConfig is similar to TestHelloWorld_yamlConfig but uses HieraConfig
+// option to explicitly define the file to use.
+func TestHelloWorld_explicitYamlConfig(t *testing.T) {
+	configOptions := make(map[string]px.Value)
+	configOptions[hieraapi.HieraConfig] = types.WrapString(`testdata/hiera.yaml`)
+
+	// Initialize a Hiera session with the ConfigLookupKey as the top-level function configured using the configOptions.
+	hiera.DoWithParent(context.Background(), provider.ConfigLookupKey, configOptions, func(c px.Context) {
+		result := hiera.Lookup(hiera.NewInvocation(c, nil, nil), `hello`, nil, nil)
+		if result == nil || `yaml data says hello` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+	})
+}
+
+// TestHelloWorld_explicitYamlConfigFile is similar to TestHelloWorld_yamlConfig but uses a combination of
+// HieraRoot and HieraConfigFileName option to find the yaml configuration file.
+func TestHelloWorld_explicitYamlConfigFile(t *testing.T) {
+	configOptions := make(map[string]px.Value)
+	configOptions[hieraapi.HieraRoot] = types.WrapString(`testdata`)
+	configOptions[hieraapi.HieraConfigFileName] = types.WrapString(`special.yaml`)
+
+	// Initialize a Hiera session with the ConfigLookupKey as the top-level function configured using the configOptions.
+	hiera.DoWithParent(context.Background(), provider.ConfigLookupKey, configOptions, func(c px.Context) {
+		result := hiera.Lookup(hiera.NewInvocation(c, nil, nil), `hello`, nil, nil)
+		if result == nil || `yaml special data says hello` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+	})
+}
+
+// TestHelloWorld_yamlAndSemiHardWired uses the MuxLookupKey to inject two lookup_key functions. The ConfigLookupKey
+// that consults the yaml config and the sayHello.
+func TestHelloWorld_yamlAndSemiHardWired(t *testing.T) {
+	configOptions := make(map[string]px.Value)
+
+	configOptions[provider.LookupKeyFunctions] = types.WrapRuntime([]hieraapi.LookupKey{provider.ConfigLookupKey, sayHello})
+	configOptions[hieraapi.HieraRoot] = types.WrapString(`testdata`)
+
+	// Initialize a Hiera session with the MuxLookupKey as the top-level function configured using the configOptions.
+	hiera.DoWithParent(context.Background(), provider.MuxLookupKey, configOptions, func(c px.Context) {
+		// A lookup of just "hello" should hit the first provider, the ConfigLookupKey.
+		result := hiera.Lookup(hiera.NewInvocation(c, nil, nil), `hello`, nil, nil)
+		if result == nil || `yaml data says hello` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+
+		// A lookup of "howdy" is not found using the yaml configuration, so it hits the second provider, the sayHello.
+		result = hiera.Lookup(hiera.NewInvocation(c, nil, nil), `howdy`, nil, nil)
+		if result == nil || `howdy world` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+	})
+}
diff --git a/examples_test/explain_test.go b/examples_test/explain_test.go
new file mode 100644
index 0000000..7afbced
--- /dev/null
+++ b/examples_test/explain_test.go
@@ -0,0 +1,148 @@
+package examples_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/lyraproj/hiera/explain"
+
+	"github.com/lyraproj/hiera/hiera"
+	"github.com/lyraproj/hiera/hieraapi"
+	"github.com/lyraproj/hiera/internal"
+	"github.com/lyraproj/hiera/provider"
+	"github.com/lyraproj/pcore/px"
+	"github.com/lyraproj/pcore/types"
+)
+
+// TestExplain shows how to provide an explainer to the invocation used when performing a lookup
+// and to extract its result.
+func TestExplain(t *testing.T) {
+	configOptions := map[string]px.Value{hieraapi.HieraRoot: types.WrapString(`testdata`)}
+	hiera.DoWithParent(context.Background(), provider.ConfigLookupKey, configOptions, func(c px.Context) {
+		// The scope type from the scope_test.go file is reused.
+		s := scope{
+			`a`: `the "a" string`,
+			`b`: 42,
+			`c`: map[string]int{`x`: 10, `y`: 20}}
+
+		// Create an explainer that excludes the lookups of lookup_options
+		explainer := explain.NewExplainer(false, false)
+
+		// Perform a lookup where the scope and explainer are included in the invocation
+		result := hiera.Lookup(internal.NewInvocation(c, s, explainer), `ipl_c`, nil, nil)
+		if result == nil || `x = 10, y = 20` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+
+		expectedExplanation := `Searching for "ipl_c"
+  Merge strategy "first found strategy"
+    data_hash function 'yaml_data'
+      Path "testdata/data.yaml"
+        Original path: "data.yaml"
+        path not found
+    data_hash function 'yaml_data'
+      Path "testdata/data/common.yaml"
+        Original path: "common.yaml"
+        Interpolation on "x = %{c.x}, y = %{c.y}"
+          Sub key: "x"
+            Found key: "x" value: 10
+          Sub key: "y"
+            Found key: "y" value: 20
+        Found key: "ipl_c" value: 'x = 10, y = 20'
+    Merged result: 'x = 10, y = 20'`
+
+		actualExplanation := explainer.String()
+		if expectedExplanation != explainer.String() {
+			t.Fatalf("expected explanation `%s` does not match actual `%s`", expectedExplanation, actualExplanation)
+		}
+	})
+}
+
+// TestExplain_withOptions shows how to configure the Explainer to include the lookup of the lookup_options
+func TestExplain_withOptions(t *testing.T) {
+	configOptions := map[string]px.Value{hieraapi.HieraRoot: types.WrapString(`testdata`)}
+	hiera.DoWithParent(context.Background(), provider.ConfigLookupKey, configOptions, func(c px.Context) {
+		s := scope{
+			`a`: `the "a" string`,
+			`b`: 42,
+			`c`: map[string]int{`x`: 10, `y`: 20}}
+
+		// Create an explainer that includes lookups of lookup_options
+		explainer := explain.NewExplainer(true, false)
+
+		// Perform a lookup where the scope and explainer are included in the invocation
+		result := hiera.Lookup(internal.NewInvocation(c, s, explainer), `ipl_c`, nil, nil)
+		if result == nil || `x = 10, y = 20` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+
+		expectedExplanation := `Searching for "lookup_options"
+  Merge strategy "deep merge strategy"
+    data_hash function 'yaml_data'
+      Path "testdata/data.yaml"
+        Original path: "data.yaml"
+        path not found
+    data_hash function 'yaml_data'
+      Path "testdata/data/common.yaml"
+        Original path: "common.yaml"
+        No such key: "lookup_options"
+Searching for "ipl_c"
+  Merge strategy "first found strategy"
+    data_hash function 'yaml_data'
+      Path "testdata/data.yaml"
+        Original path: "data.yaml"
+        path not found
+    data_hash function 'yaml_data'
+      Path "testdata/data/common.yaml"
+        Original path: "common.yaml"
+        Interpolation on "x = %{c.x}, y = %{c.y}"
+          Sub key: "x"
+            Found key: "x" value: 10
+          Sub key: "y"
+            Found key: "y" value: 20
+        Found key: "ipl_c" value: 'x = 10, y = 20'
+    Merged result: 'x = 10, y = 20'`
+
+		actualExplanation := explainer.String()
+		if expectedExplanation != explainer.String() {
+			t.Fatalf("expected explanation `%s` does not match actual `%s`", expectedExplanation, actualExplanation)
+		}
+	})
+}
+
+// TestExplain_withOnlyOptions shows how to configure the Explainer to only include the lookup of the lookup_options
+// and exclude the lookup of the actual value
+func TestExplain_withOnlyOptions(t *testing.T) {
+	configOptions := map[string]px.Value{hieraapi.HieraRoot: types.WrapString(`testdata`)}
+	hiera.DoWithParent(context.Background(), provider.ConfigLookupKey, configOptions, func(c px.Context) {
+		s := scope{
+			`a`: `the "a" string`,
+			`b`: 42,
+			`c`: map[string]int{`x`: 10, `y`: 20}}
+
+		// Create an explainer that only includes the lookups of lookup_options and excludes everything else
+		explainer := explain.NewExplainer(true, true)
+
+		// Perform a lookup where the scope and explainer are included in the invocation
+		result := hiera.Lookup(internal.NewInvocation(c, s, explainer), `ipl_c`, nil, nil)
+		if result == nil || `x = 10, y = 20` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+
+		expectedExplanation := `Searching for "lookup_options"
+  Merge strategy "deep merge strategy"
+    data_hash function 'yaml_data'
+      Path "testdata/data.yaml"
+        Original path: "data.yaml"
+        path not found
+    data_hash function 'yaml_data'
+      Path "testdata/data/common.yaml"
+        Original path: "common.yaml"
+        No such key: "lookup_options"`
+
+		actualExplanation := explainer.String()
+		if expectedExplanation != explainer.String() {
+			t.Fatalf("expected explanation `%s` does not match actual `%s`", expectedExplanation, actualExplanation)
+		}
+	})
+}
diff --git a/examples_test/merge_test.go b/examples_test/merge_test.go
new file mode 100644
index 0000000..82a2b58
--- /dev/null
+++ b/examples_test/merge_test.go
@@ -0,0 +1,93 @@
+package examples_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/lyraproj/issue/issue"
+
+	"github.com/lyraproj/dgo/util"
+
+	"github.com/lyraproj/hiera/hiera"
+	"github.com/lyraproj/hiera/hieraapi"
+	"github.com/lyraproj/hiera/internal"
+	"github.com/lyraproj/hiera/provider"
+	"github.com/lyraproj/pcore/px"
+	"github.com/lyraproj/pcore/types"
+)
+
+// TestMerge_default tests the default merge strategy which is "first found"
+func TestMerge_default(t *testing.T) {
+	configOptions := map[string]px.Value{hieraapi.HieraConfig: types.WrapString(`testdata/merge.yaml`)}
+	hiera.DoWithParent(context.Background(), provider.ConfigLookupKey, configOptions, func(c px.Context) {
+		// m.a only exists in the first provider
+		result := hiera.Lookup(internal.NewInvocation(c, nil, nil), `m.a`, nil, nil)
+		if result == nil || `first value of a` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+
+		// m.b only exists in the second provider and is hence not found since the hashes are not merged
+		err := util.Catch(func() {
+			hiera.Lookup(internal.NewInvocation(c, nil, nil), `m.b`, nil, nil)
+		})
+		re, ok := err.(issue.Reported)
+		if !(ok && re.Code() == hieraapi.NameNotFound) {
+			t.Fatalf("unexpected error %v", err)
+		}
+
+		// m.c exists in both but since no merge occurs, the first one is selected
+		result = hiera.Lookup(internal.NewInvocation(c, nil, nil), `m.c`, nil, nil)
+		if result == nil || `first value of c` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+	})
+}
+
+// TestMerge_deep shows how to pass a merge option in a lookup. The possible merge options are: First,
+// Unique, Hash, and Deep. Their behaviour should correspond to Puppet Hiera except for the current
+// limitation that Deep cannot be fine tuned with additional options. So no "knock_out_prefix" etc. just
+// yet.
+//
+// As with Puppet Hiera, merge options can also be specified as lookup_options in the data files.
+func TestMerge_deep(t *testing.T) {
+	configOptions := map[string]px.Value{hieraapi.HieraConfig: types.WrapString(`testdata/merge.yaml`)}
+	hiera.DoWithParent(context.Background(), provider.ConfigLookupKey, configOptions, func(c px.Context) {
+		// options containing the merge option "deep"
+		opts := map[string]px.Value{`merge`: px.Wrap(c, hieraapi.Deep)}
+
+		// m.a only exists in the first provider
+		result := hiera.Lookup(hiera.NewInvocation(c, nil, nil), `m.a`, nil, opts)
+		if result == nil || `first value of a` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+
+		// m.b only exists in the second provider
+		result = hiera.Lookup(hiera.NewInvocation(c, nil, nil), `m.b`, nil, opts)
+		if result == nil || `second value of b` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+
+		// m.c exists in both and since a merge occurs, the first one has precedence
+		result = hiera.Lookup(hiera.NewInvocation(c, nil, nil), `m.c`, nil, opts)
+		if result == nil || `first value of c` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+
+		// obtain the full map and compare
+		result = hiera.Lookup(hiera.NewInvocation(c, nil, nil), `m`, nil, opts)
+		if !px.Wrap(c, map[string]string{`a`: `first value of a`, `b`: `second value of b`, `c`: `first value of c`}).Equals(result, nil) {
+			t.Fatalf("unexpected result %v", result)
+		}
+
+		// obtain m and m2 using first found strategy (no options). Then merge them with an explicit call to DeepMerge
+		m := hiera.Lookup(hiera.NewInvocation(c, nil, nil), `m`, nil, nil)
+		m2 := hiera.Lookup(hiera.NewInvocation(c, nil, nil), `m2`, nil, nil)
+		result, ok := hieraapi.DeepMerge(m, m2, opts)
+		if !ok {
+			t.Fatal("DeepMerge failed")
+		}
+		if !px.Wrap(c, map[string]string{`a`: `first value of a`, `b`: `third value of b`, `c`: `first value of c`}).Equals(result, nil) {
+			t.Fatalf("unexpected result %v", result)
+		}
+	})
+}
diff --git a/examples_test/scope_test.go b/examples_test/scope_test.go
new file mode 100644
index 0000000..9e5abd0
--- /dev/null
+++ b/examples_test/scope_test.go
@@ -0,0 +1,46 @@
+package examples_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/lyraproj/hiera/hiera"
+	"github.com/lyraproj/hiera/hieraapi"
+	"github.com/lyraproj/hiera/provider"
+	"github.com/lyraproj/pcore/px"
+	"github.com/lyraproj/pcore/types"
+)
+
+// scope is a px.Keyed implementation in its simplest form
+type scope map[string]interface{}
+
+func (s scope) Get(key px.Value) (px.Value, bool) {
+	if v, ok := s[key.String()]; ok {
+		return px.Wrap(nil, v), true
+	}
+	return nil, false
+}
+
+// TestScope shows how to provide a "scope" to the invocation.
+func TestScope(t *testing.T) {
+	configOptions := map[string]px.Value{hieraapi.HieraRoot: types.WrapString(`testdata`)}
+	hiera.DoWithParent(context.Background(), provider.ConfigLookupKey, configOptions, func(c px.Context) {
+		// Our scope is just a map[string]interface{} and can hold any arbitrary data
+		s := scope{
+			`a`: `the "a" string`,
+			`b`: 42,
+			`c`: map[string]int{`x`: 10, `y`: 20}}
+
+		// The value of key "ipl_a" uses interpolation and includes %{a} in the result
+		result := hiera.Lookup(hiera.NewInvocation(c, s, nil), `ipl_a`, nil, nil)
+		if result == nil || `interpolate <the "a" string>` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+
+		// The value of key "ipl_c" is "x = %{c.x}, y = %{c.y}"
+		result = hiera.Lookup(hiera.NewInvocation(c, s, nil), `ipl_c`, nil, nil)
+		if result == nil || `x = 10, y = 20` != result.String() {
+			t.Fatalf("unexpected result %v", result)
+		}
+	})
+}
diff --git a/examples_test/testdata/data/common.yaml b/examples_test/testdata/data/common.yaml
new file mode 100644
index 0000000..224c2b8
--- /dev/null
+++ b/examples_test/testdata/data/common.yaml
@@ -0,0 +1,3 @@
+hello: yaml data says hello
+ipl_a: interpolate <%{a}>
+ipl_c: x = %{c.x}, y = %{c.y}
diff --git a/examples_test/testdata/data/merge1.yaml b/examples_test/testdata/data/merge1.yaml
new file mode 100644
index 0000000..dd4b853
--- /dev/null
+++ b/examples_test/testdata/data/merge1.yaml
@@ -0,0 +1,3 @@
+m:
+  a: first value of a
+  c: first value of c
diff --git a/examples_test/testdata/data/merge2.yaml b/examples_test/testdata/data/merge2.yaml
new file mode 100644
index 0000000..112c14c
--- /dev/null
+++ b/examples_test/testdata/data/merge2.yaml
@@ -0,0 +1,7 @@
+m:
+  a: second value of a
+  b: second value of b
+
+m2:
+  a: third value of a
+  b: third value of b
diff --git a/examples_test/testdata/hiera.yaml b/examples_test/testdata/hiera.yaml
new file mode 100644
index 0000000..0a94cf8
--- /dev/null
+++ b/examples_test/testdata/hiera.yaml
@@ -0,0 +1,1 @@
+version: 5
diff --git a/examples_test/testdata/merge.yaml b/examples_test/testdata/merge.yaml
new file mode 100644
index 0000000..c725c01
--- /dev/null
+++ b/examples_test/testdata/merge.yaml
@@ -0,0 +1,7 @@
+version: 5
+
+hierarchy:
+  - name: First
+    path: merge1.yaml
+  - name: Second
+    path: merge2.yaml
diff --git a/examples_test/testdata/special.yaml b/examples_test/testdata/special.yaml
new file mode 100644
index 0000000..9c38b9f
--- /dev/null
+++ b/examples_test/testdata/special.yaml
@@ -0,0 +1,3 @@
+version: 5
+defaults:
+  datadir: special
diff --git a/examples_test/testdata/special/common.yaml b/examples_test/testdata/special/common.yaml
new file mode 100644
index 0000000..1d418eb
--- /dev/null
+++ b/examples_test/testdata/special/common.yaml
@@ -0,0 +1,1 @@
+hello: yaml special data says hello
\ No newline at end of file