gopkg.in/juju/charm.v6-unstable@v6.0.0-20171026192109-50d0c219b496/config.go (about)

     1  // Copyright 2011, 2012, 2013 Canonical Ltd.
     2  // Licensed under the LGPLv3, 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  	"gopkg.in/yaml.v2"
    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      `yaml:"type"`
    24  	Description string      `yaml:"description,omitempty"`
    25  	Default     interface{} `yaml:"default,omitempty"`
    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  	if checker := optionTypeCheckers[option.Type]; checker != nil {
    44  		defer option.error(&err, name, value)
    45  		if value, err = checker.Coerce(value, nil); err != nil {
    46  			return nil, err
    47  		}
    48  		return value, nil
    49  	}
    50  	return nil, 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  func (option Option) parse(name, str string) (val interface{}, err error) {
    61  	switch option.Type {
    62  	case "string":
    63  		return str, nil
    64  	case "int":
    65  		val, err = strconv.ParseInt(str, 10, 64)
    66  	case "float":
    67  		val, err = strconv.ParseFloat(str, 64)
    68  	case "boolean":
    69  		val, err = strconv.ParseBool(str)
    70  	default:
    71  		return nil, fmt.Errorf("option %q has unknown type %q", name, option.Type)
    72  	}
    73  
    74  	defer option.error(&err, name, str)
    75  	return
    76  }
    77  
    78  // Config represents the supported configuration options for a charm,
    79  // as declared in its config.yaml file.
    80  type Config struct {
    81  	Options map[string]Option
    82  }
    83  
    84  // NewConfig returns a new Config without any options.
    85  func NewConfig() *Config {
    86  	return &Config{map[string]Option{}}
    87  }
    88  
    89  // ReadConfig reads a Config in YAML format.
    90  func ReadConfig(r io.Reader) (*Config, error) {
    91  	data, err := ioutil.ReadAll(r)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  	var config *Config
    96  	if err := yaml.Unmarshal(data, &config); err != nil {
    97  		return nil, err
    98  	}
    99  	if config == nil {
   100  		return nil, fmt.Errorf("invalid config: empty configuration")
   101  	}
   102  	if config.Options == nil {
   103  		// We are allowed an empty configuration if the options
   104  		// field is explicitly specified, but there is no easy way
   105  		// to tell if it was specified or not without unmarshaling
   106  		// into interface{} and explicitly checking the field.
   107  		var configInterface interface{}
   108  		if err := yaml.Unmarshal(data, &configInterface); err != nil {
   109  			return nil, err
   110  		}
   111  		m, _ := configInterface.(map[interface{}]interface{})
   112  		if _, ok := m["options"]; !ok {
   113  			return nil, fmt.Errorf("invalid config: empty configuration")
   114  		}
   115  	}
   116  	for name, option := range config.Options {
   117  		switch option.Type {
   118  		case "string", "int", "float", "boolean":
   119  		case "":
   120  			// Missing type is valid in python.
   121  			option.Type = "string"
   122  		default:
   123  			return nil, fmt.Errorf("invalid config: option %q has unknown type %q", name, option.Type)
   124  		}
   125  		def := option.Default
   126  		if def == "" && option.Type == "string" {
   127  			// Skip normal validation for compatibility with pyjuju.
   128  		} else if option.Default, err = option.validate(name, def); err != nil {
   129  			option.error(&err, name, def)
   130  			return nil, fmt.Errorf("invalid config default: %v", err)
   131  		}
   132  		config.Options[name] = option
   133  	}
   134  	return config, nil
   135  }
   136  
   137  // option returns the named option from the config, or an error if none
   138  // such exists.
   139  func (c *Config) option(name string) (Option, error) {
   140  	if option, ok := c.Options[name]; ok {
   141  		return option, nil
   142  	}
   143  	return Option{}, fmt.Errorf("unknown option %q", name)
   144  }
   145  
   146  // DefaultSettings returns settings containing the default value of every
   147  // option in the config. Default values may be nil.
   148  func (c *Config) DefaultSettings() Settings {
   149  	out := make(Settings)
   150  	for name, option := range c.Options {
   151  		out[name] = option.Default
   152  	}
   153  	return out
   154  }
   155  
   156  // ValidateSettings returns a copy of the supplied settings with a consistent type
   157  // for each value. It returns an error if the settings contain unknown keys
   158  // or invalid values.
   159  func (c *Config) ValidateSettings(settings Settings) (Settings, error) {
   160  	out := make(Settings)
   161  	for name, value := range settings {
   162  		if option, err := c.option(name); err != nil {
   163  			return nil, err
   164  		} else if value, err = option.validate(name, value); err != nil {
   165  			return nil, err
   166  		}
   167  		out[name] = value
   168  	}
   169  	return out, nil
   170  }
   171  
   172  // FilterSettings returns the subset of the supplied settings that are valid.
   173  func (c *Config) FilterSettings(settings Settings) Settings {
   174  	out := make(Settings)
   175  	for name, value := range settings {
   176  		if option, err := c.option(name); err == nil {
   177  			if value, err := option.validate(name, value); err == nil {
   178  				out[name] = value
   179  			}
   180  		}
   181  	}
   182  	return out
   183  }
   184  
   185  // ParseSettingsStrings returns settings derived from the supplied map. Every
   186  // value in the map must be parseable to the correct type for the option
   187  // identified by its key. Empty values are interpreted as nil.
   188  func (c *Config) ParseSettingsStrings(values map[string]string) (Settings, error) {
   189  	out := make(Settings)
   190  	for name, str := range values {
   191  		option, err := c.option(name)
   192  		if err != nil {
   193  			return nil, err
   194  		}
   195  		value, err := option.parse(name, str)
   196  		if err != nil {
   197  			return nil, err
   198  		}
   199  		out[name] = value
   200  	}
   201  	return out, nil
   202  }
   203  
   204  // ParseSettingsYAML returns settings derived from the supplied YAML data. The
   205  // YAML must unmarshal to a map of strings to settings data; the supplied key
   206  // must be present in the map, and must point to a map in which every value
   207  // must have, or be a string parseable to, the correct type for the associated
   208  // config option. Empty strings and nil values are both interpreted as nil.
   209  func (c *Config) ParseSettingsYAML(yamlData []byte, key string) (Settings, error) {
   210  	var allSettings map[string]Settings
   211  	if err := yaml.Unmarshal(yamlData, &allSettings); err != nil {
   212  		return nil, fmt.Errorf("cannot parse settings data: %v", err)
   213  	}
   214  	settings, ok := allSettings[key]
   215  	if !ok {
   216  		return nil, fmt.Errorf("no settings found for %q", key)
   217  	}
   218  	out := make(Settings)
   219  	for name, value := range settings {
   220  		option, err := c.option(name)
   221  		if err != nil {
   222  			return nil, err
   223  		}
   224  		// Accept string values for compatibility with python.
   225  		if str, ok := value.(string); ok {
   226  			if value, err = option.parse(name, str); err != nil {
   227  				return nil, err
   228  			}
   229  		} else if value, err = option.validate(name, value); err != nil {
   230  			return nil, err
   231  		}
   232  		out[name] = value
   233  	}
   234  	return out, nil
   235  }