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 }