Repository: ariga/atlas
Pull request #737 (2022-04-26 22:12:56)

schema/schemaspec/schemahcl: state eval with input vars
Changing direction a bit on the API for evaluating atlas DDL documents that contain input variable definitions. 


Patch:
diff --git a/schema/schemaspec/schemahcl/context.go b/schema/schemaspec/schemahcl/context.go
index 6cf7e853..f59798d5 100644
--- a/schema/schemaspec/schemahcl/context.go
+++ b/schema/schemaspec/schemahcl/context.go
@@ -32,9 +32,6 @@ import (
 //	}
 //
 func (s *State) evalCtx(ctx *hcl.EvalContext, f *hcl.File) (*hcl.EvalContext, error) {
-	if err := s.setInputVals(ctx, f.Body); err != nil {
-		return nil, err
-	}
 	c := &container{}
 	if diag := gohcl.DecodeBody(f.Body, ctx, c); diag.HasErrors() {
 		return nil, diag
@@ -46,6 +43,13 @@ func (s *State) evalCtx(ctx *hcl.EvalContext, f *hcl.File) (*hcl.EvalContext, er
 	return setBlockVars(ctx, b)
 }
 
+// varDef is an HCL resource that defines an input variable to the Atlas DDL document.
+type varDef struct {
+	Name    string    `hcl:",label"`
+	Type    cty.Value `hcl:"type"`
+	Default cty.Value `hcl:"default,optional"`
+}
+
 // setInputVals sets the input values into the evaluation context. HCL documents can define
 // input variables in the document body by defining "variable" blocks:
 //
@@ -53,25 +57,10 @@ func (s *State) evalCtx(ctx *hcl.EvalContext, f *hcl.File) (*hcl.EvalContext, er
 // 	  type = string // also supported: int, bool
 // 	  default = "rotemtam"
 // 	}
-//
-// When unmarshaling a document, input values may be provided by using the WithInputValues
-// option upon building the State:
-//
-// 	state := New(
-// 		WithInputValues(map[string]interface{}{
-// 			"name": "rotemtam",
-// 			"int":  42,
-// 			"bool": true,
-// 		}),
-// 	)
-func (s *State) setInputVals(ctx *hcl.EvalContext, body hcl.Body) error {
+func (s *State) setInputVals(ctx *hcl.EvalContext, body hcl.Body, input map[string]string) error {
 	var c struct {
-		Vars []struct {
-			Name    string    `hcl:",label"`
-			Type    cty.Value `hcl:"type"`
-			Default cty.Value `hcl:"default,optional"`
-		} `hcl:"variable,block"`
-		Remain hcl.Body `hcl:",remain"`
+		Vars   []*varDef `hcl:"variable,block"`
+		Remain hcl.Body  `hcl:",remain"`
 	}
 	nctx := ctx.NewChild()
 	nctx.Variables = map[string]cty.Value{
@@ -84,9 +73,13 @@ func (s *State) setInputVals(ctx *hcl.EvalContext, body hcl.Body) error {
 	}
 	ctxVars := make(map[string]cty.Value)
 	for _, v := range c.Vars {
-		inputVal, ok := s.config.inputValues[v.Name]
+		inputVal, ok := input[v.Name]
 		if ok {
-			ctxVars[v.Name] = inputVal
+			ctyVal, err := readVar(v, inputVal)
+			if err != nil {
+				return fmt.Errorf("failed reading var: %w", err)
+			}
+			ctxVars[v.Name] = ctyVal
 			continue
 		}
 		if v.Default == cty.NilVal {
@@ -98,6 +91,33 @@ func (s *State) setInputVals(ctx *hcl.EvalContext, body hcl.Body) error {
 	return nil
 }
 
+// readVar reads the raw inputVal as a cty.Value using the type definition on v.
+func readVar(v *varDef, inputVal string) (cty.Value, error) {
+	et := v.Type.EncapsulatedValue()
+	typ, ok := et.(*schemaspec.Type)
+	if !ok {
+		return cty.NilVal, fmt.Errorf("expected schemaspec.Type got %T", et)
+	}
+	switch typ.T {
+	case "string":
+		return cty.StringVal(inputVal), nil
+	case "int":
+		i, err := strconv.Atoi(inputVal)
+		if err != nil {
+			return cty.NilVal, err
+		}
+		return cty.NumberIntVal(int64(i)), nil
+	case "bool":
+		b, err := strconv.ParseBool(inputVal)
+		if err != nil {
+			return cty.NilVal, err
+		}
+		return cty.BoolVal(b), nil
+	default:
+		return cty.NilVal, fmt.Errorf("unknown type: %q", typ.T)
+	}
+}
+
 func capsuleTypeVal(t string) cty.Value {
 	return cty.CapsuleVal(ctyTypeSpec, &schemaspec.Type{T: t})
 }
diff --git a/schema/schemaspec/schemahcl/hcl.go b/schema/schemaspec/schemahcl/hcl.go
index 25c36e70..d1652575 100644
--- a/schema/schemaspec/schemahcl/hcl.go
+++ b/schema/schemaspec/schemahcl/hcl.go
@@ -51,8 +51,13 @@ func (s *State) MarshalSpec(v interface{}) ([]byte, error) {
 
 // UnmarshalSpec implements schemaspec.Unmarshaler.
 func (s *State) UnmarshalSpec(data []byte, v interface{}) error {
-	ctx := s.config.newCtx()
-	spec, err := s.decode(ctx, data)
+	return s.Eval(data, v, nil)
+}
+
+// Eval evaluates the Atlas DDL document with the given input and stores it in
+// the provided object.
+func (s *State) Eval(data []byte, v interface{}, input map[string]string) error {
+	spec, err := s.eval(data, input)
 	if err != nil {
 		return fmt.Errorf("schemahcl: failed decoding: %w", err)
 	}
@@ -62,8 +67,10 @@ func (s *State) UnmarshalSpec(data []byte, v interface{}) error {
 	return nil
 }
 
-// decode decodes the input Atlas HCL document and returns a *schemaspec.Resource representing it.
-func (s *State) decode(ctx *hcl.EvalContext, body []byte) (*schemaspec.Resource, error) {
+// eval evaluates the input Atlas HCL document with the given input and returns a
+// *schemaspec.Resource representing it.
+func (s *State) eval(body []byte, input map[string]string) (*schemaspec.Resource, error) {
+	ctx := s.config.newCtx()
 	parser := hclparse.NewParser()
 	srcHCL, diag := parser.ParseHCL(body, "")
 	if diag.HasErrors() {
@@ -73,6 +80,9 @@ func (s *State) decode(ctx *hcl.EvalContext, body []byte) (*schemaspec.Resource,
 		return nil, fmt.Errorf("schemahcl: no HCL syntax found in body")
 	}
 	c := &container{}
+	if err := s.setInputVals(ctx, srcHCL.Body, input); err != nil {
+		return nil, err
+	}
 	ctx, err := s.evalCtx(ctx, srcHCL)
 	if err != nil {
 		return nil, err
diff --git a/schema/schemaspec/schemahcl/opts.go b/schema/schemaspec/schemahcl/opts.go
index b9dfeb9b..30e3d5e5 100644
--- a/schema/schemaspec/schemahcl/opts.go
+++ b/schema/schemaspec/schemahcl/opts.go
@@ -14,10 +14,9 @@ import (
 type (
 	// Config configures an unmarshaling.
 	Config struct {
-		types       []*schemaspec.TypeSpec
-		newCtx      func() *hcl.EvalContext
-		pathVars    map[string]map[string]cty.Value
-		inputValues map[string]cty.Value
+		types    []*schemaspec.TypeSpec
+		newCtx   func() *hcl.EvalContext
+		pathVars map[string]map[string]cty.Value
 	}
 
 	// Option configures a Config.
@@ -95,26 +94,6 @@ func WithTypes(typeSpecs []*schemaspec.TypeSpec) Option {
 	}
 }
 
-// WithInputValues configures the input values available to the Unmarshaler.
-func WithInputValues(vals map[string]interface{}) Option {
-	inputs := make(map[string]cty.Value)
-	for k, v := range vals {
-		switch v := v.(type) {
-		case string:
-			inputs[k] = cty.StringVal(v)
-		case bool:
-			inputs[k] = cty.BoolVal(v)
-		case int:
-			inputs[k] = cty.NumberIntVal(int64(v))
-		default:
-			panic("unsupported type for input value " + k)
-		}
-	}
-	return func(config *Config) {
-		config.inputValues = inputs
-	}
-}
-
 func rawExprImpl() function.Function {
 	return function.New(&function.Spec{
 		Params: []function.Parameter{


Test patch:
diff --git a/schema/schemaspec/schemahcl/context_test.go b/schema/schemaspec/schemahcl/context_test.go
index effd52f4..e7055793 100644
--- a/schema/schemaspec/schemahcl/context_test.go
+++ b/schema/schemaspec/schemahcl/context_test.go
@@ -485,26 +485,23 @@ variable "bool" {
   type = bool
 }
 
-
 name = var.name
 default = var.default
 int = var.int
 bool = var.bool
 `
-	state := New(
-		WithInputValues(map[string]interface{}{
-			"name": "rotemtam",
-			"int":  42,
-			"bool": true,
-		}),
-	)
+	state := New()
 	var test struct {
 		Name    string `spec:"name"`
 		Default string `spec:"default"`
 		Int     int    `spec:"int"`
 		Bool    bool   `spec:"bool"`
 	}
-	err := state.UnmarshalSpec([]byte(h), &test)
+	err := state.Eval([]byte(h), &test, map[string]string{
+		"name": "rotemtam",
+		"int":  "42",
+		"bool": "true",
+	})
 	require.NoError(t, err)
 	require.EqualValues(t, "rotemtam", test.Name)
 	require.EqualValues(t, "hello", test.Default)