2011-05-01 23:10:53 +00:00
|
|
|
/*
|
|
|
|
Copyright 2011 Google Inc.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package jsonconfig
|
|
|
|
|
|
|
|
import (
|
Update from r60 to [almost] Go 1.
A lot is still broken, but most stuff at least compiles now.
The directory tree has been rearranged now too. Go libraries are now
under "pkg". Fully qualified, they are e.g. "camlistore.org/pkg/jsonsign".
The go tool cannot yet fetch from arbitrary domains, but discussion is
happening now on which mechanism to use to allow that.
For now, put the camlistore root under $GOPATH/src. Typically $GOPATH
is $HOME, so Camlistore should be at $HOME/src/camlistore.org.
Then you can:
$ go build ./server/camlistored
... etc
The build.pl script is currently disabled. It'll be resurrected at
some point, but with a very different role (helping create a fake
GOPATH and running the go build command, if things are installed at
the wrong place, and/or running fileembed generators).
Many things are certainly broken.
Many things are disabled. (MySQL, all indexing, etc).
Many things need to be moved into
camlistore.org/third_party/{code.google.com,github.com} and updated
from their r60 to Go 1 versions, where applicable.
The GoMySQL stuff should be updated to use database/sql and the ziutek
library implementing database/sql/driver.
Help wanted.
Change-Id: If71217dc5c8f0e70dbe46e9504ca5131c6eeacde
2012-02-19 05:53:06 +00:00
|
|
|
"encoding/json"
|
2014-08-02 23:53:58 +00:00
|
|
|
"errors"
|
2011-05-01 23:10:53 +00:00
|
|
|
"fmt"
|
2012-04-13 21:36:30 +00:00
|
|
|
"io"
|
2011-06-24 20:02:51 +00:00
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2011-05-01 23:10:53 +00:00
|
|
|
"regexp"
|
2013-08-28 19:27:09 +00:00
|
|
|
"runtime"
|
2011-11-26 19:37:10 +00:00
|
|
|
"strconv"
|
2014-04-02 15:00:57 +00:00
|
|
|
"strings"
|
2011-06-24 20:02:51 +00:00
|
|
|
|
Update from r60 to [almost] Go 1.
A lot is still broken, but most stuff at least compiles now.
The directory tree has been rearranged now too. Go libraries are now
under "pkg". Fully qualified, they are e.g. "camlistore.org/pkg/jsonsign".
The go tool cannot yet fetch from arbitrary domains, but discussion is
happening now on which mechanism to use to allow that.
For now, put the camlistore root under $GOPATH/src. Typically $GOPATH
is $HOME, so Camlistore should be at $HOME/src/camlistore.org.
Then you can:
$ go build ./server/camlistored
... etc
The build.pl script is currently disabled. It'll be resurrected at
some point, but with a very different role (helping create a fake
GOPATH and running the go build command, if things are installed at
the wrong place, and/or running fileembed generators).
Many things are certainly broken.
Many things are disabled. (MySQL, all indexing, etc).
Many things need to be moved into
camlistore.org/third_party/{code.google.com,github.com} and updated
from their r60 to Go 1 versions, where applicable.
The GoMySQL stuff should be updated to use database/sql and the ziutek
library implementing database/sql/driver.
Help wanted.
Change-Id: If71217dc5c8f0e70dbe46e9504ca5131c6eeacde
2012-02-19 05:53:06 +00:00
|
|
|
"camlistore.org/pkg/errorutil"
|
|
|
|
"camlistore.org/pkg/osutil"
|
2014-08-05 19:45:10 +00:00
|
|
|
"camlistore.org/pkg/wkfs"
|
2011-05-01 23:10:53 +00:00
|
|
|
)
|
|
|
|
|
2011-11-26 21:10:56 +00:00
|
|
|
type stringVector struct {
|
|
|
|
v []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *stringVector) Push(s string) {
|
|
|
|
v.v = append(v.v, s)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *stringVector) Pop() {
|
|
|
|
v.v = v.v[:len(v.v)-1]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *stringVector) Last() string {
|
|
|
|
return v.v[len(v.v)-1]
|
|
|
|
}
|
|
|
|
|
2012-04-13 22:06:04 +00:00
|
|
|
// A File is the type returned by ConfigParser.Open.
|
|
|
|
type File interface {
|
2012-04-13 21:36:30 +00:00
|
|
|
io.ReadSeeker
|
|
|
|
io.Closer
|
|
|
|
Name() string
|
|
|
|
}
|
|
|
|
|
|
|
|
// ConfigParser specifies the environment for parsing a config file
|
|
|
|
// and evaluating expressions.
|
|
|
|
type ConfigParser struct {
|
|
|
|
rootJSON Obj
|
2011-06-24 20:02:51 +00:00
|
|
|
|
|
|
|
touchedFiles map[string]bool
|
2011-11-26 21:10:56 +00:00
|
|
|
includeStack stringVector
|
2012-04-13 21:36:30 +00:00
|
|
|
|
|
|
|
// Open optionally specifies an opener function.
|
2012-04-13 22:06:04 +00:00
|
|
|
Open func(filename string) (File, error)
|
2012-04-13 21:36:30 +00:00
|
|
|
}
|
|
|
|
|
2012-04-13 22:06:04 +00:00
|
|
|
func (c *ConfigParser) open(filename string) (File, error) {
|
2012-04-13 21:36:30 +00:00
|
|
|
if c.Open == nil {
|
2014-08-05 19:45:10 +00:00
|
|
|
return wkfs.Open(filename)
|
2012-04-13 21:36:30 +00:00
|
|
|
}
|
|
|
|
return c.Open(filename)
|
2011-06-24 20:02:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validates variable names for config _env expresssions
|
2011-05-01 23:10:53 +00:00
|
|
|
var envPattern = regexp.MustCompile(`\$\{[A-Za-z0-9_]+\}`)
|
|
|
|
|
2014-08-02 23:53:58 +00:00
|
|
|
// ReadFile parses the provided path and returns the config file.
|
|
|
|
// If path is empty, the c.Open function must be defined.
|
2012-04-13 21:36:30 +00:00
|
|
|
func (c *ConfigParser) ReadFile(path string) (m map[string]interface{}, err error) {
|
2014-08-02 23:53:58 +00:00
|
|
|
if path == "" && c.Open == nil {
|
|
|
|
return nil, errors.New("ReadFile of empty string but Open hook not defined")
|
|
|
|
}
|
2012-04-13 21:36:30 +00:00
|
|
|
c.touchedFiles = make(map[string]bool)
|
|
|
|
c.rootJSON, err = c.recursiveReadJSON(path)
|
|
|
|
return c.rootJSON, err
|
|
|
|
}
|
|
|
|
|
2011-06-24 20:02:51 +00:00
|
|
|
// Decodes and evaluates a json config file, watching for include cycles.
|
2012-04-13 21:36:30 +00:00
|
|
|
func (c *ConfigParser) recursiveReadJSON(configPath string) (decodedObject map[string]interface{}, err error) {
|
2014-08-02 23:53:58 +00:00
|
|
|
if configPath != "" {
|
|
|
|
absConfigPath, err := filepath.Abs(configPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Failed to expand absolute path for %s", configPath)
|
|
|
|
}
|
|
|
|
if c.touchedFiles[absConfigPath] {
|
|
|
|
return nil, fmt.Errorf("ConfigParser include cycle detected reading config: %v",
|
|
|
|
absConfigPath)
|
|
|
|
}
|
|
|
|
c.touchedFiles[absConfigPath] = true
|
|
|
|
|
|
|
|
c.includeStack.Push(absConfigPath)
|
|
|
|
defer c.includeStack.Pop()
|
2011-06-24 20:02:51 +00:00
|
|
|
}
|
|
|
|
|
2012-04-13 22:06:04 +00:00
|
|
|
var f File
|
2012-04-13 21:36:30 +00:00
|
|
|
if f, err = c.open(configPath); err != nil {
|
2012-04-12 21:09:59 +00:00
|
|
|
return nil, fmt.Errorf("Failed to open config: %v", err)
|
2011-06-24 20:02:51 +00:00
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
decodedObject = make(map[string]interface{})
|
|
|
|
dj := json.NewDecoder(f)
|
|
|
|
if err = dj.Decode(&decodedObject); err != nil {
|
|
|
|
extra := ""
|
|
|
|
if serr, ok := err.(*json.SyntaxError); ok {
|
|
|
|
if _, serr := f.Seek(0, os.SEEK_SET); serr != nil {
|
|
|
|
log.Fatalf("seek error: %v", serr)
|
|
|
|
}
|
|
|
|
line, col, highlight := errorutil.HighlightBytePosition(f, serr.Offset)
|
|
|
|
extra = fmt.Sprintf(":\nError at line %d, column %d (file offset %d):\n%s",
|
|
|
|
line, col, serr.Offset, highlight)
|
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("error parsing JSON object in config file %s%s\n%v",
|
|
|
|
f.Name(), extra, err)
|
|
|
|
}
|
|
|
|
|
2014-04-02 15:00:57 +00:00
|
|
|
if err = c.evaluateExpressions(decodedObject, nil, false); err != nil {
|
2011-06-24 20:02:51 +00:00
|
|
|
return nil, fmt.Errorf("error expanding JSON config expressions in %s:\n%v",
|
|
|
|
f.Name(), err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return decodedObject, nil
|
|
|
|
}
|
|
|
|
|
2014-08-06 03:51:11 +00:00
|
|
|
var regFunc = map[string]expanderFunc{}
|
|
|
|
|
|
|
|
// RegisterFunc registers a new function that may be called from JSON
|
|
|
|
// configs using an array of the form ["_name", arg0, argN...].
|
|
|
|
// The provided name must begin with an underscore.
|
|
|
|
func RegisterFunc(name string, fn func(c *ConfigParser, v []interface{}) (interface{}, error)) {
|
|
|
|
if len(name) < 2 || !strings.HasPrefix(name, "_") {
|
|
|
|
panic("illegal name")
|
|
|
|
}
|
|
|
|
if _, dup := regFunc[name]; dup {
|
|
|
|
panic("duplicate registration of " + name)
|
|
|
|
}
|
|
|
|
regFunc[name] = fn
|
|
|
|
}
|
|
|
|
|
2012-04-13 21:36:30 +00:00
|
|
|
type expanderFunc func(c *ConfigParser, v []interface{}) (interface{}, error)
|
2011-11-26 20:26:42 +00:00
|
|
|
|
2014-08-06 03:51:11 +00:00
|
|
|
func namedExpander(name string) (fn expanderFunc, ok bool) {
|
2011-11-26 20:26:42 +00:00
|
|
|
switch name {
|
|
|
|
case "_env":
|
2014-08-06 03:51:11 +00:00
|
|
|
return (*ConfigParser).expandEnv, true
|
2011-11-26 20:26:42 +00:00
|
|
|
case "_fileobj":
|
2014-08-06 03:51:11 +00:00
|
|
|
return (*ConfigParser).expandFile, true
|
2011-11-26 20:26:42 +00:00
|
|
|
}
|
2014-08-06 03:51:11 +00:00
|
|
|
fn, ok = regFunc[name]
|
|
|
|
return
|
2011-11-26 20:26:42 +00:00
|
|
|
}
|
|
|
|
|
2012-04-13 21:36:30 +00:00
|
|
|
func (c *ConfigParser) evalValue(v interface{}) (interface{}, error) {
|
2011-11-26 20:26:42 +00:00
|
|
|
sl, ok := v.([]interface{})
|
|
|
|
if !ok {
|
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
if name, ok := sl[0].(string); ok {
|
|
|
|
if expander, ok := namedExpander(name); ok {
|
|
|
|
newval, err := expander(c, sl[1:])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return newval, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for i, oldval := range sl {
|
|
|
|
newval, err := c.evalValue(oldval)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
sl[i] = newval
|
|
|
|
}
|
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
|
2014-04-02 15:00:57 +00:00
|
|
|
// CheckTypes parses m and returns an error if it encounters a type or value
|
|
|
|
// that is not supported by this package.
|
|
|
|
func (c *ConfigParser) CheckTypes(m map[string]interface{}) error {
|
|
|
|
return c.evaluateExpressions(m, nil, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
// evaluateExpressions parses recursively m, populating it with the values
|
|
|
|
// that are found, unless testOnly is true.
|
|
|
|
func (c *ConfigParser) evaluateExpressions(m map[string]interface{}, seenKeys []string, testOnly bool) error {
|
2011-05-01 23:10:53 +00:00
|
|
|
for k, ei := range m {
|
2014-04-02 15:00:57 +00:00
|
|
|
thisPath := append(seenKeys, k)
|
2011-05-01 23:10:53 +00:00
|
|
|
switch subval := ei.(type) {
|
2014-08-08 13:47:10 +00:00
|
|
|
case string, bool, float64, nil:
|
2011-05-02 01:36:11 +00:00
|
|
|
continue
|
2011-05-01 23:10:53 +00:00
|
|
|
case []interface{}:
|
|
|
|
if len(subval) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2014-04-02 15:00:57 +00:00
|
|
|
evaled, err := c.evalValue(subval)
|
2011-11-26 20:26:42 +00:00
|
|
|
if err != nil {
|
2014-04-02 15:00:57 +00:00
|
|
|
return fmt.Errorf("%s: value error %v", strings.Join(thisPath, "."), err)
|
|
|
|
}
|
|
|
|
if !testOnly {
|
|
|
|
m[k] = evaled
|
2011-05-01 23:10:53 +00:00
|
|
|
}
|
|
|
|
case map[string]interface{}:
|
2014-04-02 15:00:57 +00:00
|
|
|
if err := c.evaluateExpressions(subval, thisPath, testOnly); err != nil {
|
2011-05-01 23:10:53 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
default:
|
2014-04-02 15:00:57 +00:00
|
|
|
return fmt.Errorf("%s: unhandled type %T", strings.Join(thisPath, "."), ei)
|
2011-05-01 23:10:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2011-05-09 19:12:54 +00:00
|
|
|
// Permit either:
|
|
|
|
// ["_env", "VARIABLE"] (required to be set)
|
|
|
|
// or ["_env", "VARIABLE", "default_value"]
|
2012-04-13 21:36:30 +00:00
|
|
|
func (c *ConfigParser) expandEnv(v []interface{}) (interface{}, error) {
|
2011-05-09 19:12:54 +00:00
|
|
|
hasDefault := false
|
|
|
|
def := ""
|
|
|
|
if len(v) < 1 || len(v) > 2 {
|
|
|
|
return "", fmt.Errorf("_env expansion expected 1 or 2 args, got %d", len(v))
|
2011-05-01 23:10:53 +00:00
|
|
|
}
|
|
|
|
s, ok := v[0].(string)
|
|
|
|
if !ok {
|
2011-05-09 19:12:54 +00:00
|
|
|
return "", fmt.Errorf("Expected a string after _env expansion; got %#v", v[0])
|
|
|
|
}
|
2011-11-26 19:37:10 +00:00
|
|
|
boolDefault, wantsBool := false, false
|
2011-05-09 19:12:54 +00:00
|
|
|
if len(v) == 2 {
|
|
|
|
hasDefault = true
|
2011-11-26 19:37:10 +00:00
|
|
|
switch vdef := v[1].(type) {
|
|
|
|
case string:
|
|
|
|
def = vdef
|
|
|
|
case bool:
|
|
|
|
wantsBool = true
|
|
|
|
boolDefault = vdef
|
|
|
|
default:
|
2011-05-09 19:12:54 +00:00
|
|
|
return "", fmt.Errorf("Expected default value in %q _env expansion; got %#v", s, v[1])
|
|
|
|
}
|
2011-05-01 23:10:53 +00:00
|
|
|
}
|
Update from r60 to [almost] Go 1.
A lot is still broken, but most stuff at least compiles now.
The directory tree has been rearranged now too. Go libraries are now
under "pkg". Fully qualified, they are e.g. "camlistore.org/pkg/jsonsign".
The go tool cannot yet fetch from arbitrary domains, but discussion is
happening now on which mechanism to use to allow that.
For now, put the camlistore root under $GOPATH/src. Typically $GOPATH
is $HOME, so Camlistore should be at $HOME/src/camlistore.org.
Then you can:
$ go build ./server/camlistored
... etc
The build.pl script is currently disabled. It'll be resurrected at
some point, but with a very different role (helping create a fake
GOPATH and running the go build command, if things are installed at
the wrong place, and/or running fileembed generators).
Many things are certainly broken.
Many things are disabled. (MySQL, all indexing, etc).
Many things need to be moved into
camlistore.org/third_party/{code.google.com,github.com} and updated
from their r60 to Go 1 versions, where applicable.
The GoMySQL stuff should be updated to use database/sql and the ziutek
library implementing database/sql/driver.
Help wanted.
Change-Id: If71217dc5c8f0e70dbe46e9504ca5131c6eeacde
2012-02-19 05:53:06 +00:00
|
|
|
var err error
|
2011-05-01 23:10:53 +00:00
|
|
|
expanded := envPattern.ReplaceAllStringFunc(s, func(match string) string {
|
|
|
|
envVar := match[2 : len(match)-1]
|
|
|
|
val := os.Getenv(envVar)
|
2013-08-28 19:27:09 +00:00
|
|
|
// Special case:
|
|
|
|
if val == "" && envVar == "USER" && runtime.GOOS == "windows" {
|
|
|
|
val = os.Getenv("USERNAME")
|
|
|
|
}
|
2011-05-01 23:10:53 +00:00
|
|
|
if val == "" {
|
2011-05-09 19:12:54 +00:00
|
|
|
if hasDefault {
|
|
|
|
return def
|
|
|
|
}
|
2011-05-01 23:10:53 +00:00
|
|
|
err = fmt.Errorf("couldn't expand environment variable %q", envVar)
|
|
|
|
}
|
|
|
|
return val
|
|
|
|
})
|
2011-11-26 19:37:10 +00:00
|
|
|
if wantsBool {
|
|
|
|
if expanded == "" {
|
|
|
|
return boolDefault, nil
|
|
|
|
}
|
Update from r60 to [almost] Go 1.
A lot is still broken, but most stuff at least compiles now.
The directory tree has been rearranged now too. Go libraries are now
under "pkg". Fully qualified, they are e.g. "camlistore.org/pkg/jsonsign".
The go tool cannot yet fetch from arbitrary domains, but discussion is
happening now on which mechanism to use to allow that.
For now, put the camlistore root under $GOPATH/src. Typically $GOPATH
is $HOME, so Camlistore should be at $HOME/src/camlistore.org.
Then you can:
$ go build ./server/camlistored
... etc
The build.pl script is currently disabled. It'll be resurrected at
some point, but with a very different role (helping create a fake
GOPATH and running the go build command, if things are installed at
the wrong place, and/or running fileembed generators).
Many things are certainly broken.
Many things are disabled. (MySQL, all indexing, etc).
Many things need to be moved into
camlistore.org/third_party/{code.google.com,github.com} and updated
from their r60 to Go 1 versions, where applicable.
The GoMySQL stuff should be updated to use database/sql and the ziutek
library implementing database/sql/driver.
Help wanted.
Change-Id: If71217dc5c8f0e70dbe46e9504ca5131c6eeacde
2012-02-19 05:53:06 +00:00
|
|
|
return strconv.ParseBool(expanded)
|
2011-11-26 19:37:10 +00:00
|
|
|
}
|
2011-05-01 23:10:53 +00:00
|
|
|
return expanded, err
|
|
|
|
}
|
|
|
|
|
2012-04-13 21:36:30 +00:00
|
|
|
func (c *ConfigParser) expandFile(v []interface{}) (exp interface{}, err error) {
|
2011-06-24 20:02:51 +00:00
|
|
|
if len(v) != 1 {
|
|
|
|
return "", fmt.Errorf("_file expansion expected 1 arg, got %d", len(v))
|
|
|
|
}
|
|
|
|
var incPath string
|
|
|
|
if incPath, err = osutil.FindCamliInclude(v[0].(string)); err != nil {
|
|
|
|
return "", fmt.Errorf("Included config does not exist: %v", v[0])
|
|
|
|
}
|
2011-11-26 21:10:56 +00:00
|
|
|
if exp, err = c.recursiveReadJSON(incPath); err != nil {
|
2011-06-24 20:02:51 +00:00
|
|
|
return "", fmt.Errorf("In file included from %s:\n%v",
|
|
|
|
c.includeStack.Last(), err)
|
|
|
|
}
|
|
|
|
return exp, nil
|
2011-05-01 23:10:53 +00:00
|
|
|
}
|