github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/pkg/jsonconfig/eval.go (about)

     1  /*
     2  Copyright 2011 Google Inc.
     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  	"fmt"
    22  	"io"
    23  	"log"
    24  	"os"
    25  	"path/filepath"
    26  	"regexp"
    27  	"runtime"
    28  	"strconv"
    29  	"strings"
    30  
    31  	"camlistore.org/pkg/errorutil"
    32  	"camlistore.org/pkg/osutil"
    33  )
    34  
    35  type stringVector struct {
    36  	v []string
    37  }
    38  
    39  func (v *stringVector) Push(s string) {
    40  	v.v = append(v.v, s)
    41  }
    42  
    43  func (v *stringVector) Pop() {
    44  	v.v = v.v[:len(v.v)-1]
    45  }
    46  
    47  func (v *stringVector) Last() string {
    48  	return v.v[len(v.v)-1]
    49  }
    50  
    51  // A File is the type returned by ConfigParser.Open.
    52  type File interface {
    53  	io.ReadSeeker
    54  	io.Closer
    55  	Name() string
    56  }
    57  
    58  // ConfigParser specifies the environment for parsing a config file
    59  // and evaluating expressions.
    60  type ConfigParser struct {
    61  	rootJSON Obj
    62  
    63  	touchedFiles map[string]bool
    64  	includeStack stringVector
    65  
    66  	// Open optionally specifies an opener function.
    67  	Open func(filename string) (File, error)
    68  }
    69  
    70  func (c *ConfigParser) open(filename string) (File, error) {
    71  	if c.Open == nil {
    72  		return os.Open(filename)
    73  	}
    74  	return c.Open(filename)
    75  }
    76  
    77  // Validates variable names for config _env expresssions
    78  var envPattern = regexp.MustCompile(`\$\{[A-Za-z0-9_]+\}`)
    79  
    80  func (c *ConfigParser) ReadFile(path string) (m map[string]interface{}, err error) {
    81  	c.touchedFiles = make(map[string]bool)
    82  	c.rootJSON, err = c.recursiveReadJSON(path)
    83  	return c.rootJSON, err
    84  }
    85  
    86  // Decodes and evaluates a json config file, watching for include cycles.
    87  func (c *ConfigParser) recursiveReadJSON(configPath string) (decodedObject map[string]interface{}, err error) {
    88  
    89  	absConfigPath, err := filepath.Abs(configPath)
    90  	if err != nil {
    91  		return nil, fmt.Errorf("Failed to expand absolute path for %s", configPath)
    92  	}
    93  	if c.touchedFiles[absConfigPath] {
    94  		return nil, fmt.Errorf("ConfigParser include cycle detected reading config: %v",
    95  			absConfigPath)
    96  	}
    97  	c.touchedFiles[absConfigPath] = true
    98  
    99  	c.includeStack.Push(absConfigPath)
   100  	defer c.includeStack.Pop()
   101  
   102  	var f File
   103  	if f, err = c.open(configPath); err != nil {
   104  		return nil, fmt.Errorf("Failed to open config: %v", err)
   105  	}
   106  	defer f.Close()
   107  
   108  	decodedObject = make(map[string]interface{})
   109  	dj := json.NewDecoder(f)
   110  	if err = dj.Decode(&decodedObject); err != nil {
   111  		extra := ""
   112  		if serr, ok := err.(*json.SyntaxError); ok {
   113  			if _, serr := f.Seek(0, os.SEEK_SET); serr != nil {
   114  				log.Fatalf("seek error: %v", serr)
   115  			}
   116  			line, col, highlight := errorutil.HighlightBytePosition(f, serr.Offset)
   117  			extra = fmt.Sprintf(":\nError at line %d, column %d (file offset %d):\n%s",
   118  				line, col, serr.Offset, highlight)
   119  		}
   120  		return nil, fmt.Errorf("error parsing JSON object in config file %s%s\n%v",
   121  			f.Name(), extra, err)
   122  	}
   123  
   124  	if err = c.evaluateExpressions(decodedObject, nil, false); err != nil {
   125  		return nil, fmt.Errorf("error expanding JSON config expressions in %s:\n%v",
   126  			f.Name(), err)
   127  	}
   128  
   129  	return decodedObject, nil
   130  }
   131  
   132  type expanderFunc func(c *ConfigParser, v []interface{}) (interface{}, error)
   133  
   134  func namedExpander(name string) (expanderFunc, bool) {
   135  	switch name {
   136  	case "_env":
   137  		return expanderFunc((*ConfigParser).expandEnv), true
   138  	case "_fileobj":
   139  		return expanderFunc((*ConfigParser).expandFile), true
   140  	}
   141  	return nil, false
   142  }
   143  
   144  func (c *ConfigParser) evalValue(v interface{}) (interface{}, error) {
   145  	sl, ok := v.([]interface{})
   146  	if !ok {
   147  		return v, nil
   148  	}
   149  	if name, ok := sl[0].(string); ok {
   150  		if expander, ok := namedExpander(name); ok {
   151  			newval, err := expander(c, sl[1:])
   152  			if err != nil {
   153  				return nil, err
   154  			}
   155  			return newval, nil
   156  		}
   157  	}
   158  	for i, oldval := range sl {
   159  		newval, err := c.evalValue(oldval)
   160  		if err != nil {
   161  			return nil, err
   162  		}
   163  		sl[i] = newval
   164  	}
   165  	return v, nil
   166  }
   167  
   168  // CheckTypes parses m and returns an error if it encounters a type or value
   169  // that is not supported by this package.
   170  func (c *ConfigParser) CheckTypes(m map[string]interface{}) error {
   171  	return c.evaluateExpressions(m, nil, true)
   172  }
   173  
   174  // evaluateExpressions parses recursively m, populating it with the values
   175  // that are found, unless testOnly is true.
   176  func (c *ConfigParser) evaluateExpressions(m map[string]interface{}, seenKeys []string, testOnly bool) error {
   177  	for k, ei := range m {
   178  		thisPath := append(seenKeys, k)
   179  		switch subval := ei.(type) {
   180  		case string:
   181  			continue
   182  		case bool:
   183  			continue
   184  		case float64:
   185  			continue
   186  		case []interface{}:
   187  			if len(subval) == 0 {
   188  				continue
   189  			}
   190  			evaled, err := c.evalValue(subval)
   191  			if err != nil {
   192  				return fmt.Errorf("%s: value error %v", strings.Join(thisPath, "."), err)
   193  			}
   194  			if !testOnly {
   195  				m[k] = evaled
   196  			}
   197  		case map[string]interface{}:
   198  			if err := c.evaluateExpressions(subval, thisPath, testOnly); err != nil {
   199  				return err
   200  			}
   201  		default:
   202  			return fmt.Errorf("%s: unhandled type %T", strings.Join(thisPath, "."), ei)
   203  		}
   204  	}
   205  	return nil
   206  }
   207  
   208  // Permit either:
   209  //    ["_env", "VARIABLE"] (required to be set)
   210  // or ["_env", "VARIABLE", "default_value"]
   211  func (c *ConfigParser) expandEnv(v []interface{}) (interface{}, error) {
   212  	hasDefault := false
   213  	def := ""
   214  	if len(v) < 1 || len(v) > 2 {
   215  		return "", fmt.Errorf("_env expansion expected 1 or 2 args, got %d", len(v))
   216  	}
   217  	s, ok := v[0].(string)
   218  	if !ok {
   219  		return "", fmt.Errorf("Expected a string after _env expansion; got %#v", v[0])
   220  	}
   221  	boolDefault, wantsBool := false, false
   222  	if len(v) == 2 {
   223  		hasDefault = true
   224  		switch vdef := v[1].(type) {
   225  		case string:
   226  			def = vdef
   227  		case bool:
   228  			wantsBool = true
   229  			boolDefault = vdef
   230  		default:
   231  			return "", fmt.Errorf("Expected default value in %q _env expansion; got %#v", s, v[1])
   232  		}
   233  	}
   234  	var err error
   235  	expanded := envPattern.ReplaceAllStringFunc(s, func(match string) string {
   236  		envVar := match[2 : len(match)-1]
   237  		val := os.Getenv(envVar)
   238  		// Special case:
   239  		if val == "" && envVar == "USER" && runtime.GOOS == "windows" {
   240  			val = os.Getenv("USERNAME")
   241  		}
   242  		if val == "" {
   243  			if hasDefault {
   244  				return def
   245  			}
   246  			err = fmt.Errorf("couldn't expand environment variable %q", envVar)
   247  		}
   248  		return val
   249  	})
   250  	if wantsBool {
   251  		if expanded == "" {
   252  			return boolDefault, nil
   253  		}
   254  		return strconv.ParseBool(expanded)
   255  	}
   256  	return expanded, err
   257  }
   258  
   259  func (c *ConfigParser) expandFile(v []interface{}) (exp interface{}, err error) {
   260  	if len(v) != 1 {
   261  		return "", fmt.Errorf("_file expansion expected 1 arg, got %d", len(v))
   262  	}
   263  	var incPath string
   264  	if incPath, err = osutil.FindCamliInclude(v[0].(string)); err != nil {
   265  		return "", fmt.Errorf("Included config does not exist: %v", v[0])
   266  	}
   267  	if exp, err = c.recursiveReadJSON(incPath); err != nil {
   268  		return "", fmt.Errorf("In file included from %s:\n%v",
   269  			c.includeStack.Last(), err)
   270  	}
   271  	return exp, nil
   272  }