github.com/camlistore/go4@v0.0.0-20200104003542-c7e774b10ea0/jsonconfig/eval.go (about)

     1  /*
     2  Copyright 2011 The go4 Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package jsonconfig
    18  
    19  import (
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"log"
    25  	"os"
    26  	"path/filepath"
    27  	"regexp"
    28  	"runtime"
    29  	"strconv"
    30  	"strings"
    31  
    32  	"go4.org/errorutil"
    33  	"go4.org/wkfs"
    34  )
    35  
    36  type stringVector struct {
    37  	v []string
    38  }
    39  
    40  func (v *stringVector) Push(s string) {
    41  	v.v = append(v.v, s)
    42  }
    43  
    44  func (v *stringVector) Pop() {
    45  	v.v = v.v[:len(v.v)-1]
    46  }
    47  
    48  func (v *stringVector) Last() string {
    49  	return v.v[len(v.v)-1]
    50  }
    51  
    52  // A File is the type returned by ConfigParser.Open.
    53  type File interface {
    54  	io.ReadSeeker
    55  	io.Closer
    56  	Name() string
    57  }
    58  
    59  // ConfigParser specifies the environment for parsing a config file
    60  // and evaluating expressions.
    61  type ConfigParser struct {
    62  	rootJSON Obj
    63  
    64  	touchedFiles map[string]bool
    65  	includeStack stringVector
    66  
    67  	// Open optionally specifies an opener function.
    68  	Open func(filename string) (File, error)
    69  
    70  	// IncludeDirs optionally specifies where to find the other config files which are child
    71  	// objects of this config, if any. Even if nil, the working directory is always searched
    72  	// first.
    73  	IncludeDirs []string
    74  }
    75  
    76  func (c *ConfigParser) open(filename string) (File, error) {
    77  	if c.Open == nil {
    78  		return wkfs.Open(filename)
    79  	}
    80  	return c.Open(filename)
    81  }
    82  
    83  // Validates variable names for config _env expresssions
    84  var envPattern = regexp.MustCompile(`\$\{[A-Za-z0-9_]+\}`)
    85  
    86  // ReadFile parses the provided path and returns the config file.
    87  // If path is empty, the c.Open function must be defined.
    88  func (c *ConfigParser) ReadFile(path string) (Obj, error) {
    89  	if path == "" && c.Open == nil {
    90  		return nil, errors.New("ReadFile of empty string but Open hook not defined")
    91  	}
    92  	c.touchedFiles = make(map[string]bool)
    93  	var err error
    94  	c.rootJSON, err = c.recursiveReadJSON(path)
    95  	return c.rootJSON, err
    96  }
    97  
    98  // Decodes and evaluates a json config file, watching for include cycles.
    99  func (c *ConfigParser) recursiveReadJSON(configPath string) (decodedObject map[string]interface{}, err error) {
   100  	if configPath != "" {
   101  		absConfigPath, err := filepath.Abs(configPath)
   102  		if err != nil {
   103  			return nil, fmt.Errorf("Failed to expand absolute path for %s", configPath)
   104  		}
   105  		if c.touchedFiles[absConfigPath] {
   106  			return nil, fmt.Errorf("ConfigParser include cycle detected reading config: %v",
   107  				absConfigPath)
   108  		}
   109  		c.touchedFiles[absConfigPath] = true
   110  
   111  		c.includeStack.Push(absConfigPath)
   112  		defer c.includeStack.Pop()
   113  	}
   114  
   115  	var f File
   116  	if f, err = c.open(configPath); err != nil {
   117  		return nil, fmt.Errorf("Failed to open config: %v", err)
   118  	}
   119  	defer f.Close()
   120  
   121  	decodedObject = make(map[string]interface{})
   122  	dj := json.NewDecoder(f)
   123  	if err = dj.Decode(&decodedObject); err != nil {
   124  		extra := ""
   125  		if serr, ok := err.(*json.SyntaxError); ok {
   126  			if _, serr := f.Seek(0, os.SEEK_SET); serr != nil {
   127  				log.Fatalf("seek error: %v", serr)
   128  			}
   129  			line, col, highlight := errorutil.HighlightBytePosition(f, serr.Offset)
   130  			extra = fmt.Sprintf(":\nError at line %d, column %d (file offset %d):\n%s",
   131  				line, col, serr.Offset, highlight)
   132  		}
   133  		return nil, fmt.Errorf("error parsing JSON object in config file %s%s\n%v",
   134  			f.Name(), extra, err)
   135  	}
   136  
   137  	if err = c.evaluateExpressions(decodedObject, nil, false); err != nil {
   138  		return nil, fmt.Errorf("error expanding JSON config expressions in %s:\n%v",
   139  			f.Name(), err)
   140  	}
   141  
   142  	return decodedObject, nil
   143  }
   144  
   145  var regFunc = map[string]expanderFunc{}
   146  
   147  // RegisterFunc registers a new function that may be called from JSON
   148  // configs using an array of the form ["_name", arg0, argN...].
   149  // The provided name must begin with an underscore.
   150  func RegisterFunc(name string, fn func(c *ConfigParser, v []interface{}) (interface{}, error)) {
   151  	if len(name) < 2 || !strings.HasPrefix(name, "_") {
   152  		panic("illegal name")
   153  	}
   154  	if _, dup := regFunc[name]; dup {
   155  		panic("duplicate registration of " + name)
   156  	}
   157  	regFunc[name] = fn
   158  }
   159  
   160  type expanderFunc func(c *ConfigParser, v []interface{}) (interface{}, error)
   161  
   162  func namedExpander(name string) (fn expanderFunc, ok bool) {
   163  	switch name {
   164  	case "_env":
   165  		return (*ConfigParser).expandEnv, true
   166  	case "_fileobj":
   167  		return (*ConfigParser).expandFile, true
   168  	}
   169  	fn, ok = regFunc[name]
   170  	return
   171  }
   172  
   173  func (c *ConfigParser) evalValue(v interface{}) (interface{}, error) {
   174  	sl, ok := v.([]interface{})
   175  	if !ok {
   176  		return v, nil
   177  	}
   178  	if name, ok := sl[0].(string); ok {
   179  		if expander, ok := namedExpander(name); ok {
   180  			newval, err := expander(c, sl[1:])
   181  			if err != nil {
   182  				return nil, err
   183  			}
   184  			return newval, nil
   185  		}
   186  	}
   187  	for i, oldval := range sl {
   188  		newval, err := c.evalValue(oldval)
   189  		if err != nil {
   190  			return nil, err
   191  		}
   192  		sl[i] = newval
   193  	}
   194  	return v, nil
   195  }
   196  
   197  // CheckTypes parses m and returns an error if it encounters a type or value
   198  // that is not supported by this package.
   199  func (c *ConfigParser) CheckTypes(m map[string]interface{}) error {
   200  	return c.evaluateExpressions(m, nil, true)
   201  }
   202  
   203  // evaluateExpressions parses recursively m, populating it with the values
   204  // that are found, unless testOnly is true.
   205  func (c *ConfigParser) evaluateExpressions(m map[string]interface{}, seenKeys []string, testOnly bool) error {
   206  	for k, ei := range m {
   207  		thisPath := append(seenKeys, k)
   208  		switch subval := ei.(type) {
   209  		case string, bool, float64, nil:
   210  			continue
   211  		case []interface{}:
   212  			if len(subval) == 0 {
   213  				continue
   214  			}
   215  			evaled, err := c.evalValue(subval)
   216  			if err != nil {
   217  				return fmt.Errorf("%s: value error %v", strings.Join(thisPath, "."), err)
   218  			}
   219  			if !testOnly {
   220  				m[k] = evaled
   221  			}
   222  		case map[string]interface{}:
   223  			if err := c.evaluateExpressions(subval, thisPath, testOnly); err != nil {
   224  				return err
   225  			}
   226  		default:
   227  			return fmt.Errorf("%s: unhandled type %T", strings.Join(thisPath, "."), ei)
   228  		}
   229  	}
   230  	return nil
   231  }
   232  
   233  // Permit either:
   234  //    ["_env", "VARIABLE"] (required to be set)
   235  // or ["_env", "VARIABLE", "default_value"]
   236  func (c *ConfigParser) expandEnv(v []interface{}) (interface{}, error) {
   237  	hasDefault := false
   238  	def := ""
   239  	if len(v) < 1 || len(v) > 2 {
   240  		return "", fmt.Errorf("_env expansion expected 1 or 2 args, got %d", len(v))
   241  	}
   242  	s, ok := v[0].(string)
   243  	if !ok {
   244  		return "", fmt.Errorf("Expected a string after _env expansion; got %#v", v[0])
   245  	}
   246  	boolDefault, wantsBool := false, false
   247  	if len(v) == 2 {
   248  		hasDefault = true
   249  		switch vdef := v[1].(type) {
   250  		case string:
   251  			def = vdef
   252  		case bool:
   253  			wantsBool = true
   254  			boolDefault = vdef
   255  		default:
   256  			return "", fmt.Errorf("Expected default value in %q _env expansion; got %#v", s, v[1])
   257  		}
   258  	}
   259  	var err error
   260  	expanded := envPattern.ReplaceAllStringFunc(s, func(match string) string {
   261  		envVar := match[2 : len(match)-1]
   262  		val := os.Getenv(envVar)
   263  		// Special case:
   264  		if val == "" && envVar == "USER" && runtime.GOOS == "windows" {
   265  			val = os.Getenv("USERNAME")
   266  		}
   267  		if val == "" {
   268  			if hasDefault {
   269  				return def
   270  			}
   271  			err = fmt.Errorf("couldn't expand environment variable %q", envVar)
   272  		}
   273  		return val
   274  	})
   275  	if wantsBool {
   276  		if expanded == "" {
   277  			return boolDefault, nil
   278  		}
   279  		return strconv.ParseBool(expanded)
   280  	}
   281  	return expanded, err
   282  }
   283  
   284  func (c *ConfigParser) expandFile(v []interface{}) (exp interface{}, err error) {
   285  	if len(v) != 1 {
   286  		return "", fmt.Errorf("_file expansion expected 1 arg, got %d", len(v))
   287  	}
   288  	var incPath string
   289  	if incPath, err = c.ConfigFilePath(v[0].(string)); err != nil {
   290  		return "", fmt.Errorf("Included config does not exist: %v", v[0])
   291  	}
   292  	if exp, err = c.recursiveReadJSON(incPath); err != nil {
   293  		return "", fmt.Errorf("In file included from %s:\n%v",
   294  			c.includeStack.Last(), err)
   295  	}
   296  	return exp, nil
   297  }
   298  
   299  // ConfigFilePath checks if configFile is found and returns a usable path to it.
   300  // It first checks if configFile is an absolute path, or if it's found in the
   301  // current working directory. If not, it then checks if configFile is in one of
   302  // c.IncludeDirs. It returns an error if configFile is absolute and could not be
   303  // statted, or os.ErrNotExist if configFile was not found.
   304  func (c *ConfigParser) ConfigFilePath(configFile string) (path string, err error) {
   305  	// Try to open as absolute / relative to CWD
   306  	_, err = os.Stat(configFile)
   307  	if err != nil && filepath.IsAbs(configFile) {
   308  		return "", err
   309  	}
   310  	if err == nil {
   311  		return configFile, nil
   312  	}
   313  
   314  	for _, d := range c.IncludeDirs {
   315  		if _, err := os.Stat(filepath.Join(d, configFile)); err == nil {
   316  			return filepath.Join(d, configFile), nil
   317  		}
   318  	}
   319  
   320  	return "", os.ErrNotExist
   321  }