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