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 }