Repository: lyraproj/hiera
Pull request #31 (2019-07-16 13:21:46)

Enable rendering of explanation output
This commit turns the node objects used by the explainer into pcore
objects so that they can be serialized properly. The serialization is
then used when a combination of --explain and --render-as is given on
the command line. This enables explanation output to be rendered as
JSON or YAML which in turn is a first step towards a distributed Hiera
with full explanation support.


Patch:
diff --git a/explain/explainer.go b/explain/explainer.go
index d040000..b875739 100644
--- a/explain/explainer.go
+++ b/explain/explainer.go
@@ -1,15 +1,15 @@
 package explain
 
 import (
-	"github.com/lyraproj/pcore/utils"
-
 	"github.com/lyraproj/hiera/hieraapi"
 	"github.com/lyraproj/pcore/px"
+	"github.com/lyraproj/pcore/utils"
 )
 
 type Context int
 
 type Explainer interface {
+	px.Value
 	utils.Indentable
 
 	// AcceptFound accepts information that a value was found for a given key
diff --git a/go.mod b/go.mod
index 8388bc5..2f4ca7e 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/labstack/echo v3.3.10+incompatible
 	github.com/labstack/gommon v0.2.9 // indirect
 	github.com/lyraproj/issue v0.0.0-20190606092846-e082d6813d15
-	github.com/lyraproj/pcore v0.0.0-20190709110110-57c90466e307
+	github.com/lyraproj/pcore v0.0.0-20190716112816-4f6c9937c850
 	github.com/spf13/cobra v0.0.4
 	github.com/stretchr/testify v1.3.0
 	gopkg.in/yaml.v2 v2.2.2
diff --git a/go.sum b/go.sum
index 9cf8f81..f7cf4c0 100644
--- a/go.sum
+++ b/go.sum
@@ -29,6 +29,12 @@ github.com/lyraproj/pcore v0.0.0-20190619162937-645af37a80ad h1:Smdt4D4MrveCvMVg
 github.com/lyraproj/pcore v0.0.0-20190619162937-645af37a80ad/go.mod h1:yqZNNXXw/2It5Jh2Vc+g93Qm+1q6EklXJ0t/xqNNBV8=
 github.com/lyraproj/pcore v0.0.0-20190709110110-57c90466e307 h1:Dr/NfFG/2mEgleyfsiLT+9j7olm5imQkynsDRpGL8Tk=
 github.com/lyraproj/pcore v0.0.0-20190709110110-57c90466e307/go.mod h1:yqZNNXXw/2It5Jh2Vc+g93Qm+1q6EklXJ0t/xqNNBV8=
+github.com/lyraproj/pcore v0.0.0-20190716055636-c093587ebc58 h1:KVwA8JT513CGkM6SVLxGIfqeLUgW3p+dDq1UEC7lYG8=
+github.com/lyraproj/pcore v0.0.0-20190716055636-c093587ebc58/go.mod h1:yqZNNXXw/2It5Jh2Vc+g93Qm+1q6EklXJ0t/xqNNBV8=
+github.com/lyraproj/pcore v0.0.0-20190716091017-ba8ad962370f h1:vHGIY4Wj2Sf3V1z2JtafmU45Ook/eoJdTWKxyWkgtXU=
+github.com/lyraproj/pcore v0.0.0-20190716091017-ba8ad962370f/go.mod h1:yqZNNXXw/2It5Jh2Vc+g93Qm+1q6EklXJ0t/xqNNBV8=
+github.com/lyraproj/pcore v0.0.0-20190716112816-4f6c9937c850 h1:AcpbJKP33rWGYEtKeCZIaEDDcVT8sDVDU6NUsQxT738=
+github.com/lyraproj/pcore v0.0.0-20190716112816-4f6c9937c850/go.mod h1:yqZNNXXw/2It5Jh2Vc+g93Qm+1q6EklXJ0t/xqNNBV8=
 github.com/lyraproj/semver v0.0.0-20181213164306-02ecea2cd6a2 h1:vb4PbiMtIXdhsOUinkkcqZiASDIZzXRhSG4yvNfE0tg=
 github.com/lyraproj/semver v0.0.0-20181213164306-02ecea2cd6a2/go.mod h1:KOdZKnEBdDb2iGPUnHiKpk3M5cvv949xMyj8XPqaMF0=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
diff --git a/hiera/hiera.go b/hiera/hiera.go
index 9a5f048..07fa9de 100644
--- a/hiera/hiera.go
+++ b/hiera/hiera.go
@@ -16,7 +16,6 @@ import (
 	"github.com/lyraproj/pcore/pcore"
 	"github.com/lyraproj/pcore/px"
 	"github.com/lyraproj/pcore/types"
-	"github.com/lyraproj/pcore/utils"
 	"github.com/lyraproj/pcore/yaml"
 )
 
@@ -149,21 +148,16 @@ func LookupAndRender(c px.Context, opts *CommandOptions, args []string, out io.W
 
 	var explainer explain.Explainer
 	if opts.ExplainData || opts.ExplainOptions {
-		if opts.RenderAs != `` {
-			var ex string
-			if opts.ExplainData {
-				ex = `explain`
-			} else {
-				ex = `explain-options`
-			}
-			panic(fmt.Errorf(`--render-as is mutually exclusive to --%s`, ex))
-		}
 		explainer = explain.NewExplainer(opts.ExplainOptions, opts.ExplainOptions && !opts.ExplainData)
 	}
 
 	found := Lookup2(internal.NewInvocation(c, scope, explainer), args, tp, dv, nil, nil, options, nil)
 	if explainer != nil {
-		utils.Fprintln(out, explainer)
+		renderAs := Text
+		if opts.RenderAs != `` {
+			renderAs = RenderName(opts.RenderAs)
+		}
+		Render(c, renderAs, explainer, out)
 		return found != nil
 	}
 
diff --git a/hieraapi/location.go b/hieraapi/location.go
index 713a21b..c859a3b 100644
--- a/hieraapi/location.go
+++ b/hieraapi/location.go
@@ -12,7 +12,7 @@ const LcMappedPaths = LocationKind(`mapped_paths`)
 type Location interface {
 	fmt.Stringer
 	Kind() LocationKind
-	Exist() bool
+	Exists() bool
 	Resolve(ic Invocation, dataDir string) []Location
 	Original() string
 	Resolved() string
diff --git a/hieraapi/mergestrategy.go b/hieraapi/mergestrategy.go
index ef8d8a3..89d864a 100644
--- a/hieraapi/mergestrategy.go
+++ b/hieraapi/mergestrategy.go
@@ -22,6 +22,8 @@ var GetMergeStrategy func(name MergeStrategyName, options map[string]px.Value) M
 type MergeStrategy interface {
 	issue.Labeled
 
+	Name() MergeStrategyName
+
 	// Lookup performs a series of lookups for each variant found in the given variants slice. The actual
 	// lookup value is returned by the given value function which will be called at least once. The argument to
 	// the value function will be an element of the variants slice.
diff --git a/internal/datadigprovider.go b/internal/datadigprovider.go
index 639f485..1299d74 100644
--- a/internal/datadigprovider.go
+++ b/internal/datadigprovider.go
@@ -35,7 +35,7 @@ func (dh *DataDigProvider) invokeWithLocation(invocation hieraapi.Invocation, lo
 		return dh.lookupKey(invocation, nil, key)
 	}
 	result := invocation.WithLocation(location, func() px.Value {
-		if location.Exist() {
+		if location.Exists() {
 			return dh.lookupKey(invocation, location, key)
 		}
 		invocation.ReportLocationNotFound()
diff --git a/internal/datahashprovider.go b/internal/datahashprovider.go
index 937540b..e07583d 100644
--- a/internal/datahashprovider.go
+++ b/internal/datahashprovider.go
@@ -39,7 +39,7 @@ func (dh *DataHashProvider) invokeWithLocation(invocation hieraapi.Invocation, l
 		return dh.lookupKey(invocation, nil, root)
 	}
 	return invocation.WithLocation(location, func() px.Value {
-		if location.Exist() {
+		if location.Exists() {
 			return dh.lookupKey(invocation, location, root)
 		}
 		invocation.ReportLocationNotFound()
diff --git a/internal/explainer.go b/internal/explainer.go
index bda4b0b..16aa2bf 100644
--- a/internal/explainer.go
+++ b/internal/explainer.go
@@ -2,14 +2,15 @@ package internal
 
 import (
 	"fmt"
+	"io"
 	"strconv"
 
-	"github.com/lyraproj/hiera/explain"
-
-	"github.com/lyraproj/pcore/utils"
+	"github.com/lyraproj/pcore/types"
 
+	"github.com/lyraproj/hiera/explain"
 	"github.com/lyraproj/hiera/hieraapi"
 	"github.com/lyraproj/pcore/px"
+	"github.com/lyraproj/pcore/utils"
 )
 
 func init() {
@@ -28,6 +29,7 @@ const (
 )
 
 type explainNode interface {
+	px.Value
 	utils.Indentable
 	appendBranch(branch explainNode)
 	appendText(text string)
@@ -54,6 +56,226 @@ type explainTreeNode struct {
 	k  string
 }
 
+var explainNodeMetaType px.ObjectType
+var explainDataProviderMetaType px.ObjectType
+var explainInterpolateMetaType px.ObjectType
+var explainInvalidKeyMetaType px.ObjectType
+var explainKeySegmentMetaType px.ObjectType
+var explainLocationMetaType px.ObjectType
+var explainLookupMetaType px.ObjectType
+var explainMergeMetaType px.ObjectType
+var explainMergeSourceMetaType px.ObjectType
+var explainSubLookupMetaType px.ObjectType
+var explainerMetaType px.ObjectType
+
+func init() {
+	explainNodeMetaType = px.NewObjectType(`Hiera::ExplainNode`, `{
+		attributes => {
+      branches => Optional[Array[Hiera::ExplainNode]],
+      texts => Optional[Array[String]],
+      event => Optional[Integer[1,6]],
+      value => { type => RichData, value => undef },
+      key => Optional[String]
+    }
+	}`)
+
+	explainDataProviderMetaType = px.NewObjectType(`Hiera::ExplainProvider`, `Hiera::ExplainNode{
+		attributes => {
+      providerName => String
+    }
+	}`,
+		types.NoPositionalConstructor,
+		func(c px.Context, args []px.Value) px.Value {
+			en := &explainDataProvider{}
+			en.initialize(args[0].(px.OrderedMap))
+			return en
+		})
+
+	explainInterpolateMetaType = px.NewObjectType(`Hiera::ExplainInterpolate`, `Hiera::ExplainNode{
+		attributes => {
+      expression => String
+    }
+	}`,
+		types.NoPositionalConstructor,
+		func(c px.Context, args []px.Value) px.Value {
+			en := &explainInterpolate{}
+			en.initialize(args[0].(px.OrderedMap))
+			return en
+		})
+
+	explainInvalidKeyMetaType = px.NewObjectType(`Hiera::ExplainInvalidKey`, `Hiera::ExplainNode{}`,
+		types.NoPositionalConstructor,
+		func(c px.Context, args []px.Value) px.Value {
+			en := &explainInvalidKey{}
+			en.initialize(args[0].(px.OrderedMap))
+			return en
+		})
+
+	explainKeySegmentMetaType = px.NewObjectType(`Hiera::ExplainKeySegment`, `Hiera::ExplainNode{
+		attributes => {
+			segment => Variant[String,Integer]
+		}
+	}`,
+		types.NoPositionalConstructor,
+		func(c px.Context, args []px.Value) px.Value {
+			en := &explainKeySegment{}
+			en.initialize(args[0].(px.OrderedMap))
+			return en
+		})
+
+	explainLocationMetaType = px.NewObjectType(`Hiera::ExplainLocation`, `Hiera::ExplainNode{
+		attributes => {
+			location => Hiera::Location
+		}
+	}`,
+		types.NoPositionalConstructor,
+		func(c px.Context, args []px.Value) px.Value {
+			en := &explainLocation{}
+			en.initialize(args[0].(px.OrderedMap))
+			return en
+		})
+
+	explainLookupMetaType = px.NewObjectType(`Hiera::ExplainLookup`, `Hiera::ExplainNode{}`,
+		types.NoPositionalConstructor,
+		func(c px.Context, args []px.Value) px.Value {
+			en := &explainLookup{}
+			en.initialize(args[0].(px.OrderedMap))
+			return en
+		})
+
+	explainMergeMetaType = px.NewObjectType(`Hiera::ExplainMerge`, `Hiera::ExplainNode{
+		attributes => {
+			strategy => String,
+      options => { type => Hash[String,Data], value => {} }
+		}
+	}`,
+		types.NoPositionalConstructor,
+		func(c px.Context, args []px.Value) px.Value {
+			en := &explainMerge{}
+			en.initialize(args[0].(px.OrderedMap))
+			return en
+		})
+
+	explainMergeSourceMetaType = px.NewObjectType(`Hiera::ExplainMergeSource`, `Hiera::ExplainNode{
+		attributes => {
+			mergeSource => String
+		}
+	}`,
+		types.NoPositionalConstructor,
+		func(c px.Context, args []px.Value) px.Value {
+			en := &explainMergeSource{}
+			en.initialize(args[0].(px.OrderedMap))
+			return en
+		})
+
+	explainSubLookupMetaType = px.NewObjectType(`Hiera::ExplainSubLookup`, `Hiera::ExplainNode{
+		attributes => {
+			subKey => String
+		}
+	}`,
+		types.NoPositionalConstructor,
+		func(c px.Context, args []px.Value) px.Value {
+			en := &explainSubLookup{}
+			en.initialize(args[0].(px.OrderedMap))
+			return en
+		})
+
+	explainerMetaType = px.NewObjectType(`Hiera::Explainer`, `Hiera::ExplainNode{
+		attributes => {
+			current => Optional[Hiera::ExplainNode],
+			options => { type => Boolean, value => false },
+      onlyOptions => { type => Boolean, value => false },
+		}
+	}`,
+		types.NoPositionalConstructor,
+		func(c px.Context, args []px.Value) px.Value {
+			en := &explainer{}
+			en.initialize(args[0].(px.OrderedMap))
+			return en
+		})
+}
+
+func (en *explainTreeNode) initialize(ih px.OrderedMap) {
+	if v, ok := ih.Get4(`branches`); ok {
+		ba := v.(px.List)
+		bs := make([]explainNode, ba.Len())
+		ba.EachWithIndex(func(b px.Value, i int) {
+			bx := b.(*explainTreeNode)
+			bx.p = en
+			bs[i] = bx
+		})
+		en.bs = bs
+	}
+	if v, ok := ih.Get4(`texts`); ok {
+		ta := v.(px.List)
+		ts := make([]string, ta.Len())
+		ta.EachWithIndex(func(t px.Value, i int) {
+			ts[i] = t.String()
+		})
+		en.ts = ts
+	}
+	if v, ok := ih.Get4(`event`); ok {
+		en.e = event(v.(px.Integer).Int())
+	}
+	if v, ok := ih.Get4(`value`); ok {
+		en.v = v
+	}
+	if v, ok := ih.Get4(`key`); ok {
+		en.k = v.String()
+	}
+}
+
+func (en *explainTreeNode) String() string {
+	return px.ToString(en)
+}
+
+func (en *explainTreeNode) Equals(value interface{}, guard px.Guard) bool {
+	return en == value
+}
+
+func (en *explainTreeNode) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	types.ObjectToString(en, format, bld, g)
+}
+
+func (en *explainTreeNode) PType() px.Type {
+	return explainNodeMetaType
+}
+
+func (en *explainTreeNode) Get(key string) (value px.Value, ok bool) {
+	switch key {
+	case `branches`:
+		if len(en.bs) > 0 {
+			return px.Wrap(nil, en.bs), true
+		}
+		return px.Undef, true
+	case `texts`:
+		if len(en.ts) > 0 {
+			return px.Wrap(nil, en.ts), true
+		}
+		return px.Undef, true
+	case `event`:
+		if en.e > 0 {
+			return types.WrapInteger(int64(en.e)), true
+		}
+		return px.Undef, true
+	case `value`:
+		if en.v != nil {
+			return en.v, true
+		}
+		return px.Undef, true
+	case `key`:
+		if en.k != `` {
+			return types.WrapString(en.k), true
+		}
+		return px.Undef, true
+	}
+	return nil, false
+}
+
+func (en *explainTreeNode) InitHash() px.OrderedMap {
+	return explainNodeMetaType.InstanceHash(en)
+}
+
 func keyToString(k interface{}) string {
 	switch k := k.(type) {
 	case string:
@@ -176,12 +398,17 @@ func (en *explainTreeNode) value() px.Value {
 
 type explainDataProvider struct {
 	explainTreeNode
-	provider hieraapi.DataProvider
+	providerName string
+}
+
+func (en *explainDataProvider) initialize(ih px.OrderedMap) {
+	en.explainTreeNode.initialize(ih)
+	en.providerName = ih.Get5(`providerName`, px.EmptyString).String()
 }
 
 func (en *explainDataProvider) AppendTo(w *utils.Indenter) {
 	w.NewLine()
-	w.Append(en.provider.FullName())
+	w.Append(en.providerName)
 	w = w.Indent()
 	en.dumpBranches(w)
 	en.dumpOutcome(w)
@@ -191,11 +418,62 @@ func (en *explainDataProvider) String() string {
 	return utils.IndentedString(en)
 }
 
+func (en *explainDataProvider) Equals(value interface{}, guard px.Guard) bool {
+	return en == value
+}
+
+func (en *explainDataProvider) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	types.ObjectToString(en, format, bld, g)
+}
+
+func (en *explainDataProvider) PType() px.Type {
+	return explainDataProviderMetaType
+}
+
+func (en *explainDataProvider) Get(key string) (value px.Value, ok bool) {
+	if key == `providerName` {
+		return types.WrapString(en.providerName), true
+	}
+	return en.explainTreeNode.Get(key)
+}
+
+func (en *explainDataProvider) InitHash() px.OrderedMap {
+	return explainDataProviderMetaType.InstanceHash(en)
+}
+
 type explainInterpolate struct {
 	explainTreeNode
 	expression string
 }
 
+func (en *explainInterpolate) initialize(ih px.OrderedMap) {
+	en.explainTreeNode.initialize(ih)
+	en.expression = ih.Get5(`expression`, px.EmptyString).String()
+}
+
+func (en *explainInterpolate) Equals(value interface{}, guard px.Guard) bool {
+	return en == value
+}
+
+func (en *explainInterpolate) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	types.ObjectToString(en, format, bld, g)
+}
+
+func (en *explainInterpolate) PType() px.Type {
+	return explainInterpolateMetaType
+}
+
+func (en *explainInterpolate) Get(key string) (value px.Value, ok bool) {
+	if key == `expression` {
+		return types.WrapString(en.expression), true
+	}
+	return en.explainTreeNode.Get(key)
+}
+
+func (en *explainInterpolate) InitHash() px.OrderedMap {
+	return explainInterpolateMetaType.InstanceHash(en)
+}
+
 func (en *explainInterpolate) AppendTo(w *utils.Indenter) {
 	w.NewLine()
 	w.Append(`Interpolation on "`)
@@ -212,6 +490,26 @@ type explainInvalidKey struct {
 	explainTreeNode
 }
 
+func (en *explainInvalidKey) Equals(value interface{}, guard px.Guard) bool {
+	return en == value
+}
+
+func (en *explainInvalidKey) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	types.ObjectToString(en, format, bld, g)
+}
+
+func (en *explainInvalidKey) String() string {
+	return utils.IndentedString(en)
+}
+
+func (en *explainInvalidKey) PType() px.Type {
+	return explainInvalidKeyMetaType
+}
+
+func (en *explainInvalidKey) InitHash() px.OrderedMap {
+	return explainInvalidKeyMetaType.InstanceHash(en)
+}
+
 func (en *explainInvalidKey) AppendTo(w *utils.Indenter) {
 	w.NewLine()
 	w.Append(`Invalid key "`)
@@ -219,15 +517,44 @@ func (en *explainInvalidKey) AppendTo(w *utils.Indenter) {
 	w.AppendRune('"')
 }
 
-func (en *explainInvalidKey) String() string {
-	return utils.IndentedString(en)
-}
-
 type explainKeySegment struct {
 	explainTreeNode
 	segment interface{}
 }
 
+func (en *explainKeySegment) initialize(ih px.OrderedMap) {
+	en.explainTreeNode.initialize(ih)
+	sg := ih.Get5(`expression`, px.EmptyString)
+	if i, ok := sg.(px.Integer); ok {
+		en.segment = i.Int()
+	} else {
+		en.segment = sg.String()
+	}
+}
+
+func (en *explainKeySegment) Equals(value interface{}, guard px.Guard) bool {
+	return en == value
+}
+
+func (en *explainKeySegment) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	types.ObjectToString(en, format, bld, g)
+}
+
+func (en *explainKeySegment) PType() px.Type {
+	return explainKeySegmentMetaType
+}
+
+func (en *explainKeySegment) Get(key string) (value px.Value, ok bool) {
+	if key == `segment` {
+		return px.Wrap(nil, en.segment), true
+	}
+	return en.explainTreeNode.Get(key)
+}
+
+func (en *explainKeySegment) InitHash() px.OrderedMap {
+	return explainKeySegmentMetaType.InstanceHash(en)
+}
+
 func (en *explainKeySegment) AppendTo(w *utils.Indenter) {
 	en.dumpOutcome(w)
 }
@@ -241,6 +568,36 @@ type explainLocation struct {
 	location hieraapi.Location
 }
 
+func (en *explainLocation) initialize(ih px.OrderedMap) {
+	en.explainTreeNode.initialize(ih)
+	if loc, ok := ih.Get4(`location`); ok {
+		en.location = loc.(hieraapi.Location)
+	}
+}
+
+func (en *explainLocation) Equals(value interface{}, guard px.Guard) bool {
+	return en == value
+}
+
+func (en *explainLocation) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	types.ObjectToString(en, format, bld, g)
+}
+
+func (en *explainLocation) PType() px.Type {
+	return explainLocationMetaType
+}
+
+func (en *explainLocation) Get(key string) (value px.Value, ok bool) {
+	if key == `location` {
+		return en.location.(px.Value), true
+	}
+	return en.explainTreeNode.Get(key)
+}
+
+func (en *explainLocation) InitHash() px.OrderedMap {
+	return explainLocationMetaType.InstanceHash(en)
+}
+
 func (en *explainLocation) AppendTo(w *utils.Indenter) {
 	w.NewLine()
 	if en.location.Kind() == hieraapi.LcPath {
@@ -276,6 +633,22 @@ type explainLookup struct {
 	explainTreeNode
 }
 
+func (en *explainLookup) Equals(value interface{}, guard px.Guard) bool {
+	return en == value
+}
+
+func (en *explainLookup) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	types.ObjectToString(en, format, bld, g)
+}
+
+func (en *explainLookup) PType() px.Type {
+	return explainLookupMetaType
+}
+
+func (en *explainLookup) InitHash() px.OrderedMap {
+	return explainLookupMetaType.InstanceHash(en)
+}
+
 func (en *explainLookup) AppendTo(w *utils.Indenter) {
 	if w.Len() > 0 || w.Level() > 0 {
 		w.NewLine()
@@ -295,6 +668,45 @@ type explainMerge struct {
 	merge hieraapi.MergeStrategy
 }
 
+func (en *explainMerge) initialize(ih px.OrderedMap) {
+	en.explainTreeNode.initialize(ih)
+	name := hieraapi.First
+	var opts map[string]px.Value
+	if v, ok := ih.Get4(`strategy`); ok {
+		name = hieraapi.MergeStrategyName(v.String())
+	}
+	if v, ok := ih.Get4(`options`); ok {
+		opts = v.(px.OrderedMap).ToStringMap()
+	}
+	en.merge = getMergeStrategy(name, opts)
+}
+
+func (en *explainMerge) Equals(value interface{}, guard px.Guard) bool {
+	return en == value
+}
+
+func (en *explainMerge) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	types.ObjectToString(en, format, bld, g)
+}
+
+func (en *explainMerge) PType() px.Type {
+	return explainMergeMetaType
+}
+
+func (en *explainMerge) Get(key string) (value px.Value, ok bool) {
+	switch key {
+	case `strategy`:
+		return types.WrapString(string(en.merge.Name())), true
+	case `options`:
+		return en.merge.Options(), true
+	}
+	return en.explainTreeNode.Get(key)
+}
+
+func (en *explainMerge) InitHash() px.OrderedMap {
+	return explainMergeMetaType.InstanceHash(en)
+}
+
 func (en *explainMerge) AppendTo(w *utils.Indenter) {
 	switch len(en.bs) {
 	case 0:
@@ -331,6 +743,36 @@ type explainMergeSource struct {
 	mergeSource string
 }
 
+func (en *explainMergeSource) initialize(ih px.OrderedMap) {
+	en.explainTreeNode.initialize(ih)
+	if ms, ok := ih.Get4(`mergeSource`); ok {
+		en.mergeSource = ms.String()
+	}
+}
+
+func (en *explainMergeSource) Equals(value interface{}, guard px.Guard) bool {
+	return en == value
+}
+
+func (en *explainMergeSource) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	types.ObjectToString(en, format, bld, g)
+}
+
+func (en *explainMergeSource) PType() px.Type {
+	return explainMergeSourceMetaType
+}
+
+func (en *explainMergeSource) Get(key string) (value px.Value, ok bool) {
+	if key == `mergeSource` {
+		return types.WrapString(en.mergeSource), true
+	}
+	return en.explainTreeNode.Get(key)
+}
+
+func (en *explainMergeSource) InitHash() px.OrderedMap {
+	return explainMergeSourceMetaType.InstanceHash(en)
+}
+
 func (en *explainMergeSource) AppendTo(w *utils.Indenter) {
 	w.NewLine()
 	w.Append(`Using merge options from `)
@@ -346,6 +788,36 @@ type explainSubLookup struct {
 	subKey hieraapi.Key
 }
 
+func (en *explainSubLookup) initialize(ih px.OrderedMap) {
+	en.explainTreeNode.initialize(ih)
+	if ms, ok := ih.Get4(`subKey`); ok {
+		en.subKey = newKey(ms.String())
+	}
+}
+
+func (en *explainSubLookup) Equals(value interface{}, guard px.Guard) bool {
+	return en == value
+}
+
+func (en *explainSubLookup) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	types.ObjectToString(en, format, bld, g)
+}
+
+func (en *explainSubLookup) PType() px.Type {
+	return explainSubLookupMetaType
+}
+
+func (en *explainSubLookup) Get(key string) (value px.Value, ok bool) {
+	if key == `subKey` {
+		return types.WrapString(en.subKey.String()), true
+	}
+	return en.explainTreeNode.Get(key)
+}
+
+func (en *explainSubLookup) InitHash() px.OrderedMap {
+	return explainSubLookupMetaType.InstanceHash(en)
+}
+
 func (en *explainSubLookup) AppendTo(w *utils.Indenter) {
 	w.NewLine()
 	w.Append(`Sub key: "`)
@@ -378,6 +850,54 @@ func newExplainer(options, onlyOptions bool) explain.Explainer {
 	return ex
 }
 
+func (en *explainer) initialize(ih px.OrderedMap) {
+	en.explainTreeNode.initialize(ih)
+	if v, ok := ih.Get4(`current`); ok {
+		if en.current != en {
+			en.current = v.(explainNode)
+		}
+	} else {
+		en.current = en
+	}
+	if v, ok := ih.Get4(`options`); ok {
+		en.options = v.(px.Boolean).Bool()
+	}
+	if v, ok := ih.Get4(`onlyOptions`); ok {
+		en.onlyOptions = v.(px.Boolean).Bool()
+	}
+}
+
+func (en *explainer) Equals(value interface{}, guard px.Guard) bool {
+	return en == value
+}
+
+func (en *explainer) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	types.ObjectToString(en, format, bld, g)
+}
+
+func (en *explainer) PType() px.Type {
+	return explainerMetaType
+}
+
+func (en *explainer) Get(key string) (value px.Value, ok bool) {
+	switch key {
+	case `current`:
+		if en.current != en {
+			return en.current, true
+		}
+		return px.Undef, true
+	case `options`:
+		return types.WrapBoolean(en.options), true
+	case `onlyOptions`:
+		return types.WrapBoolean(en.onlyOptions), true
+	}
+	return en.explainTreeNode.Get(key)
+}
+
+func (en *explainer) InitHash() px.OrderedMap {
+	return explainerMetaType.InstanceHash(en)
+}
+
 func (ex *explainer) AcceptFound(key interface{}, value px.Value) {
 	ex.current.found(key, value)
 }
@@ -413,7 +933,7 @@ func (ex *explainer) AcceptText(text string) {
 }
 
 func (ex *explainer) PushDataProvider(pvd hieraapi.DataProvider) {
-	en := &explainDataProvider{provider: pvd}
+	en := &explainDataProvider{providerName: pvd.FullName()}
 	en.p = ex.current
 	ex.current.appendBranch(en)
 	ex.current = en
diff --git a/internal/location.go b/internal/location.go
index 4a0bba9..9f23fa3 100644
--- a/internal/location.go
+++ b/internal/location.go
@@ -2,6 +2,7 @@ package internal
 
 import (
 	"fmt"
+	"io"
 	"os"
 	"path/filepath"
 
@@ -14,11 +15,82 @@ import (
 type path struct {
 	original string
 	resolved string
-	exist    bool
+	exists   bool
 }
 
-func (p *path) Exist() bool {
-	return p.exist
+var pathMetaType px.ObjectType
+var uriMetaType px.ObjectType
+
+func init() {
+	px.NewObjectType(`Hiera::Location`, `{
+    attributes => {
+      original => String,
+      resolved => String,
+      exists => Boolean
+    }
+  }`)
+	pathMetaType = px.NewObjectType(`Hiera::Path`, `Hiera::Location{
+    constants => {
+      kind => path 
+    }
+  }`,
+		func(c px.Context, args []px.Value) px.Value {
+			return &path{args[0].String(), args[1].String(), args[2].(px.Boolean).Bool()}
+		},
+		func(c px.Context, args []px.Value) px.Value {
+			m, _ := args[0].(px.OrderedMap)
+			return &path{m.Get5(`original`, nil).String(), m.Get5(`resolved`, nil).String(), m.Get5(`exists`, nil).(px.Boolean).Bool()}
+		},
+	)
+	uriMetaType = px.NewObjectType(`Hiera::URI`, `Hiera::Location{
+    constants => {
+      kind => uri 
+    }
+  }`,
+		func(c px.Context, args []px.Value) px.Value {
+			return &uri{args[0].String(), args[1].String()}
+		},
+		func(c px.Context, args []px.Value) px.Value {
+			m, _ := args[0].(px.OrderedMap)
+			return &uri{m.Get5(`original`, nil).String(), m.Get5(`resolved`, nil).String()}
+		},
+	)
+}
+
+func (p *path) Equals(value interface{}, guard px.Guard) bool {
+	op, ok := value.(*path)
+	if ok {
+		ok = *p == *op
+	}
+	return ok
+}
+
+func (p *path) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	px.ToString(p)
+}
+
+func (p *path) PType() px.Type {
+	return pathMetaType
+}
+
+func (p *path) Get(key string) (value px.Value, ok bool) {
+	switch key {
+	case `original`:
+		return types.WrapString(p.original), true
+	case `resolved`:
+		return types.WrapString(p.resolved), true
+	case `exists`:
+		return types.WrapBoolean(p.exists), true
+	}
+	return nil, false
+}
+
+func (p *path) InitHash() px.OrderedMap {
+	return pathMetaType.InstanceHash(p)
+}
+
+func (p *path) Exists() bool {
+	return p.exists
 }
 
 func (p *path) Kind() hieraapi.LocationKind {
@@ -26,7 +98,7 @@ func (p *path) Kind() hieraapi.LocationKind {
 }
 
 func (p *path) String() string {
-	return fmt.Sprintf("path{ original:%s, resolved:%s, exist:%v}", p.original, p.resolved, p.exist)
+	return fmt.Sprintf("path{ original:%s, resolved:%s, exist:%v}", p.original, p.resolved, p.exists)
 }
 
 func (p *path) Resolve(ic hieraapi.Invocation, dataDir string) []hieraapi.Location {
@@ -48,7 +120,7 @@ type glob struct {
 	pattern string
 }
 
-func (g *glob) Exist() bool {
+func (g *glob) Exists() bool {
 	return false
 }
 
@@ -85,7 +157,37 @@ type uri struct {
 	resolved string
 }
 
-func (u *uri) Exist() bool {
+func (u *uri) Equals(value interface{}, guard px.Guard) bool {
+	ou, ok := value.(*uri)
+	if ok {
+		ok = *u == *ou
+	}
+	return ok
+}
+
+func (u *uri) ToString(bld io.Writer, format px.FormatContext, g px.RDetect) {
+	px.ToString(u)
+}
+
+func (u *uri) PType() px.Type {
+	return uriMetaType
+}
+
+func (u *uri) Get(key string) (value px.Value, ok bool) {
+	switch key {
+	case `original`:
+		return types.WrapString(u.original), true
+	case `resolved`:
+		return types.WrapString(u.resolved), true
+	}
+	return nil, false
+}
+
+func (u *uri) InitHash() px.OrderedMap {
+	return uriMetaType.InstanceHash(u)
+}
+
+func (u *uri) Exists() bool {
 	return true
 }
 
@@ -121,7 +223,7 @@ type mappedPaths struct {
 	template string
 }
 
-func (m *mappedPaths) Exist() bool {
+func (m *mappedPaths) Exists() bool {
 	return false
 }
 
diff --git a/internal/lookupkeyprovider.go b/internal/lookupkeyprovider.go
index 4b7b72d..efa088e 100644
--- a/internal/lookupkeyprovider.go
+++ b/internal/lookupkeyprovider.go
@@ -38,7 +38,7 @@ func (dh *LookupKeyProvider) invokeWithLocation(invocation hieraapi.Invocation,
 		return dh.lookupKey(invocation, nil, root)
 	}
 	return invocation.WithLocation(location, func() px.Value {
-		if location.Exist() {
+		if location.Exists() {
 			return dh.lookupKey(invocation, location, root)
 		}
 		invocation.ReportLocationNotFound()
diff --git a/internal/mergestrategy.go b/internal/mergestrategy.go
index 04199ca..65a649c 100644
--- a/internal/mergestrategy.go
+++ b/internal/mergestrategy.go
@@ -87,6 +87,10 @@ func variantLookup(v reflect.Value, vf func(l interface{}) px.Value) px.Value {
 	return nil
 }
 
+func (d *firstFound) Name() hieraapi.MergeStrategyName {
+	return hieraapi.First
+}
+
 func (d *firstFound) Label() string {
 	return `first found strategy`
 }
@@ -135,6 +139,10 @@ func (d *firstFound) merge(a, b px.Value) px.Value {
 	return a
 }
 
+func (d *unique) Name() hieraapi.MergeStrategyName {
+	return hieraapi.Unique
+}
+
 func (d *unique) Label() string {
 	return `unique merge strategy`
 }
@@ -166,6 +174,10 @@ func (d *unique) merge(a, b px.Value) px.Value {
 	return d.convertValue(a).(px.List).AddAll(d.convertValue(b).(px.List)).Unique()
 }
 
+func (d *deepMerge) Name() hieraapi.MergeStrategyName {
+	return hieraapi.Deep
+}
+
 func (d *deepMerge) Label() string {
 	return `deep merge strategy`
 }
@@ -194,6 +206,10 @@ func (d *deepMerge) merge(a, b px.Value) px.Value {
 	return v
 }
 
+func (d *hashMerge) Name() hieraapi.MergeStrategyName {
+	return hieraapi.Hash
+}
+
 func (d *hashMerge) Label() string {
 	return `hash merge strategy`
 }


Test patch:
diff --git a/lookup/lookup_test.go b/lookup/lookup_test.go
index 29bd392..1300f8f 100644
--- a/lookup/lookup_test.go
+++ b/lookup/lookup_test.go
@@ -113,7 +113,7 @@ func TestLookup_explain(t *testing.T) {
         No such key: "interpolate_ca"
     data_hash function 'yaml_data'
       Path "[^"]*/testdata/hiera/named_by_fact\.yaml"
-        Original path: "named_%{data_file}.yaml"
+        Original path: "named_%\{data_file\}.yaml"
         Interpolation on "This is %\{c\.a\}"
           Sub key: "a"
             Found key: "a" value: 'value of c.a'
@@ -123,6 +123,60 @@ func TestLookup_explain(t *testing.T) {
 	})
 }
 
+func TestLookup_explain_yaml(t *testing.T) {
+	inTestdata(func() {
+		result, err := executeLookup(`--explain`, `--facts`, `facts.yaml`, `--render-as`, `yaml`, `interpolate_ca`)
+		require.NoError(t, err)
+		require.Regexp(t,
+			`\A__ptype: Hiera::Explainer
+branches:
+  - __ptype: Hiera::ExplainLookup
+    branches:
+      - __ptype: Hiera::ExplainMerge
+        branches:
+          - __ptype: Hiera::ExplainProvider
+            branches:
+              - __ptype: Hiera::ExplainLocation
+                event: 5
+                key: interpolate_ca
+                location:
+                    __ptype: Hiera::Path
+                    exists: true
+                    original: common\.yaml
+                    resolved: .*/testdata/hiera/common\.yaml
+            providerName: data_hash function 'yaml_data'
+          - __ptype: Hiera::ExplainProvider
+            branches:
+              - __ptype: Hiera::ExplainLocation
+                branches:
+                  - __ptype: Hiera::ExplainInterpolate
+                    branches:
+                      - __ptype: Hiera::ExplainSubLookup
+                        branches:
+                          - __ptype: Hiera::ExplainKeySegment
+                            event: 1
+                            key: a
+                            segment: a
+                            value: value of c\.a
+                        subKey: c\.a
+                    expression: This is %\{c\.a\}
+                event: 1
+                key: interpolate_ca
+                location:
+                    __ptype: Hiera::Path
+                    exists: true
+                    original: named_%\{data_file\}\.yaml
+                    resolved: .*/testdata/hiera/named_by_fact\.yaml
+                value: This is value of c\.a
+            providerName: data_hash function 'yaml_data'
+        event: 6
+        strategy: first
+        value: This is value of c\.a
+    key: interpolate_ca
+\z`, string(result))
+	})
+}
+
 func TestLookup_explain_options(t *testing.T) {
 	inTestdata(func() {
 		result, err := executeLookup(`--explain-options`, `--facts`, `facts.yaml`, `hash`)