github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/charm/config.go (about)

     1  // Copyright 2011, 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charm
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"strconv"
    11  
    12  	"github.com/juju/schema"
    13  	"launchpad.net/goyaml"
    14  )
    15  
    16  // Settings is a group of charm config option names and values. A Settings
    17  // S is considered valid by the Config C if every key in S is an option in
    18  // C, and every value either has the correct type or is nil.
    19  type Settings map[string]interface{}
    20  
    21  // Option represents a single charm config option.
    22  type Option struct {
    23  	Type        string
    24  	Description string
    25  	Default     interface{}
    26  }
    27  
    28  // error replaces any supplied non-nil error with a new error describing a
    29  // validation failure for the supplied value.
    30  func (option Option) error(err *error, name string, value interface{}) {
    31  	if *err != nil {
    32  		*err = fmt.Errorf("option %q expected %s, got %#v", name, option.Type, value)
    33  	}
    34  }
    35  
    36  // validate returns an appropriately-typed value for the supplied value, or
    37  // returns an error if it cannot be converted to the correct type. Nil values
    38  // are always considered valid.
    39  func (option Option) validate(name string, value interface{}) (_ interface{}, err error) {
    40  	if value == nil {
    41  		return nil, nil
    42  	}
    43  	defer option.error(&err, name, value)
    44  	if checker := optionTypeCheckers[option.Type]; checker != nil {
    45  		if value, err = checker.Coerce(value, nil); err != nil {
    46  			return nil, err
    47  		}
    48  		return value, nil
    49  	}
    50  	panic(fmt.Errorf("option %q has unknown type %q", name, option.Type))
    51  }
    52  
    53  var optionTypeCheckers = map[string]schema.Checker{
    54  	"string":  schema.String(),
    55  	"int":     schema.Int(),
    56  	"float":   schema.Float(),
    57  	"boolean": schema.Bool(),
    58  }
    59  
    60  // parse returns an appropriately-typed value for the supplied string, or
    61  // returns an error if it cannot be parsed to the correct type.
    62  func (option Option) parse(name, str string) (_ interface{}, err error) {
    63  	defer option.error(&err, name, str)
    64  	switch option.Type {
    65  	case "string":
    66  		return str, nil
    67  	case "int":
    68  		return strconv.ParseInt(str, 10, 64)
    69  	case "float":
    70  		return strconv.ParseFloat(str, 64)
    71  	case "boolean":
    72  		return strconv.ParseBool(str)
    73  	}
    74  	panic(fmt.Errorf("option %q has unknown type %q", name, option.Type))
    75  }
    76  
    77  // Config represents the supported configuration options for a charm,
    78  // as declared in its config.yaml file.
    79  type Config struct {
    80  	Options map[string]Option
    81  }
    82  
    83  // NewConfig returns a new Config without any options.
    84  func NewConfig() *Config {
    85  	return &Config{map[string]Option{}}
    86  }
    87  
    88  // ReadConfig reads a Config in YAML format.
    89  func ReadConfig(r io.Reader) (*Config, error) {
    90  	data, err := ioutil.ReadAll(r)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	var config *Config
    95  	if err := goyaml.Unmarshal(data, &config); err != nil {
    96  		return nil, err
    97  	}
    98  	if config == nil {
    99  		return nil, fmt.Errorf("invalid config: empty configuration")
   100  	}
   101  	for name, option := range config.Options {
   102  		switch option.Type {
   103  		case "string", "int", "float", "boolean":
   104  		case "":
   105  			// Missing type is valid in python.
   106  			option.Type = "string"
   107  		default:
   108  			return nil, fmt.Errorf("invalid config: option %q has unknown type %q", name, option.Type)
   109  		}
   110  		def := option.Default
   111  		if def == "" && option.Type == "string" {
   112  			// Skip normal validation for compatibility with pyjuju.
   113  		} else if option.Default, err = option.validate(name, def); err != nil {
   114  			option.error(&err, name, def)
   115  			return nil, fmt.Errorf("invalid config default: %v", err)
   116  		}
   117  		config.Options[name] = option
   118  	}
   119  	return config, nil
   120  }
   121  
   122  // option returns the named option from the config, or an error if none
   123  // such exists.
   124  func (c *Config) option(name string) (Option, error) {
   125  	if option, ok := c.Options[name]; ok {
   126  		return option, nil
   127  	}
   128  	return Option{}, fmt.Errorf("unknown option %q", name)
   129  }
   130  
   131  // DefaultSettings returns settings containing the default value of every
   132  // option in the config. Default values may be nil.
   133  func (c *Config) DefaultSettings() Settings {
   134  	out := make(Settings)
   135  	for name, option := range c.Options {
   136  		out[name] = option.Default
   137  	}
   138  	return out
   139  }
   140  
   141  // ValidateSettings returns a copy of the supplied settings with a consistent type
   142  // for each value. It returns an error if the settings contain unknown keys
   143  // or invalid values.
   144  func (c *Config) ValidateSettings(settings Settings) (Settings, error) {
   145  	out := make(Settings)
   146  	for name, value := range settings {
   147  		if option, err := c.option(name); err != nil {
   148  			return nil, err
   149  		} else if value, err = option.validate(name, value); err != nil {
   150  			return nil, err
   151  		}
   152  		out[name] = value
   153  	}
   154  	return out, nil
   155  }
   156  
   157  // FilterSettings returns the subset of the supplied settings that are valid.
   158  func (c *Config) FilterSettings(settings Settings) Settings {
   159  	out := make(Settings)
   160  	for name, value := range settings {
   161  		if option, err := c.option(name); err == nil {
   162  			if value, err := option.validate(name, value); err == nil {
   163  				out[name] = value
   164  			}
   165  		}
   166  	}
   167  	return out
   168  }
   169  
   170  // ParseSettingsStrings returns settings derived from the supplied map. Every
   171  // value in the map must be parseable to the correct type for the option
   172  // identified by its key. Empty values are interpreted as nil.
   173  func (c *Config) ParseSettingsStrings(values map[string]string) (Settings, error) {
   174  	out := make(Settings)
   175  	for name, str := range values {
   176  		option, err := c.option(name)
   177  		if err != nil {
   178  			return nil, err
   179  		}
   180  		value, err := option.parse(name, str)
   181  		if err != nil {
   182  			return nil, err
   183  		}
   184  		out[name] = value
   185  	}
   186  	return out, nil
   187  }
   188  
   189  // ParseSettingsYAML returns settings derived from the supplied YAML data. The
   190  // YAML must unmarshal to a map of strings to settings data; the supplied key
   191  // must be present in the map, and must point to a map in which every value
   192  // must have, or be a string parseable to, the correct type for the associated
   193  // config option. Empty strings and nil values are both interpreted as nil.
   194  func (c *Config) ParseSettingsYAML(yamlData []byte, key string) (Settings, error) {
   195  	var allSettings map[string]Settings
   196  	if err := goyaml.Unmarshal(yamlData, &allSettings); err != nil {
   197  		return nil, fmt.Errorf("cannot parse settings data: %v", err)
   198  	}
   199  	settings, ok := allSettings[key]
   200  	if !ok {
   201  		return nil, fmt.Errorf("no settings found for %q", key)
   202  	}
   203  	out := make(Settings)
   204  	for name, value := range settings {
   205  		option, err := c.option(name)
   206  		if err != nil {
   207  			return nil, err
   208  		}
   209  		// Accept string values for compatibility with python.
   210  		if str, ok := value.(string); ok {
   211  			if value, err = option.parse(name, str); err != nil {
   212  				return nil, err
   213  			}
   214  		} else if value, err = option.validate(name, value); err != nil {
   215  			return nil, err
   216  		}
   217  		out[name] = value
   218  	}
   219  	return out, nil
   220  }