├── .gitignore ├── .rspec ├── .travis.yml ├── .yardopts ├── CHANGELOG.textile ├── FEATURES.txt ├── Gemfile ├── Guardfile ├── LICENSE.md ├── README.textile ├── Rakefile ├── VERSION ├── bin ├── configliere ├── configliere-decrypt ├── configliere-delete ├── configliere-dump ├── configliere-encrypt ├── configliere-list └── configliere-set ├── configliere.gemspec ├── examples ├── config_block_script.rb ├── encrypted_script.rb ├── env_var_script.rb ├── help_message_demo.rb ├── independent_config.rb ├── joke.rb ├── prompt.rb ├── simple_script.rb └── simple_script.yaml ├── lib ├── configliere.rb └── configliere │ ├── commandline.rb │ ├── commands.rb │ ├── config_block.rb │ ├── config_file.rb │ ├── crypter.rb │ ├── deep_hash.rb │ ├── define.rb │ ├── encrypted.rb │ ├── env_var.rb │ ├── param.rb │ ├── prompt.rb │ └── vayacondios.rb ├── pom.xml ├── spec ├── configliere │ ├── commandline_spec.rb │ ├── commands_spec.rb │ ├── config_block_spec.rb │ ├── config_file_spec.rb │ ├── deep_hash_spec.rb │ ├── define_spec.rb │ ├── encrypted_spec.rb │ ├── env_var_spec.rb │ ├── param_spec.rb │ └── prompt_spec.rb ├── configliere_spec.rb ├── spec.opts └── spec_helper.rb └── src └── main └── java └── com └── infochimps └── config ├── Configliere.java └── IntegrationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | ## OS 2 | .DS_Store 3 | Icon 4 | nohup.out 5 | .bak 6 | 7 | *.pem 8 | 9 | ## EDITORS 10 | \#* 11 | .\#* 12 | \#*\# 13 | *~ 14 | *.swp 15 | REVISION 16 | TAGS* 17 | tmtags 18 | *_flymake.* 19 | *_flymake 20 | *.tmproj 21 | .project 22 | .settings 23 | 24 | ## COMPILED 25 | a.out 26 | *.o 27 | *.pyc 28 | *.so 29 | target/ 30 | 31 | ## OTHER SCM 32 | .bzr 33 | .hg 34 | .svn 35 | 36 | ## PROJECT::GENERAL 37 | 38 | log/* 39 | tmp/* 40 | pkg/* 41 | 42 | coverage 43 | rdoc 44 | doc 45 | pkg 46 | .rake_test_cache 47 | .bundle 48 | .yardoc 49 | 50 | .vendor 51 | 52 | ## PROJECT::SPECIFIC 53 | 54 | Gemfile.lock 55 | .rvmrc 56 | .rbenv-version 57 | .rbx 58 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 1.9.2 5 | - jruby-19mode 6 | - rbx-19mode 7 | - ruby-head 8 | - jruby-head 9 | # 1.8 still works! keep it that way 10 | - jruby-18mode 11 | - rbx-18mode 12 | - 1.8.7 13 | - ree 14 | 15 | bundler_args: --without docs support 16 | 17 | notifications: 18 | email: false 19 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --readme README.textile 2 | --markup markdown 3 | - 4 | VERSION 5 | CHANGELOG.textile 6 | LICENSE.md 7 | README.textile 8 | -------------------------------------------------------------------------------- /CHANGELOG.textile: -------------------------------------------------------------------------------- 1 | h2. Version 0.4.10 2 | 3 | Minor improvement to the sub-command (git-style) module. Still not great. 4 | 5 | h2. Version 0.4.9 6 | 7 | Gemfile dependencies less aggressive. 8 | Uses MultiJSON; removed strict dependency on JSON 9 | 10 | h2. Version 0.4.7 11 | 12 | * can now load JSON 13 | * updated specs to run on Windows 14 | * Only accepts things that are to_sym'bolizable as keys 15 | * pings rdoc.info on commit 16 | 17 | h2. Version 0.4.6 18 | 19 | Unwittingly moved DeepHash's alias_method(:merge,:update) *above* the redefinition of update, and so made merge be quietly broken. Fixed, and many more specs added. 20 | 21 | h2. Version 0.4.5 22 | 23 | * can now ask for @has_definition?(:my_param)@ or @has_definition?(:my_param, :description)@. 24 | * validate! and define each returns self, so you can chain. 25 | 26 | h2. Version 0.4.4 2011-05-13: MANY BREAKING CHANGES 27 | 28 | Killed many features for great justice. Configliere should now be much more predictable and lightweight, and has a much stricter API. Unfortunately, this will break backwards compatibility in some ways. 29 | 30 | h4. Cleanup of the Configliere::Params "Magic Hash" class 31 | 32 | * Configliere no longer modifies any core classes 33 | * All the hash gymnastics have been relocated to DeepHash. 34 | * DeepHash now converts *all* keys to symbols, and transparently handles deep keys: 35 | 36 |
 37 |   Settings['foo.bar'] = 1
 38 |   Settings.merge! 'foo.bar' => 1
 39 |   Settings[ [:foo, :bar] ] = 1
 40 |   # => all give { :foo => { :bar => 1 } }
 41 | 
42 | 43 | * DeepHash typeconverts objects of type Hash to DeepHash when merged or added. 44 | * DeepHash added several convenience methods -- @slice/slice!@, @compact/compact!@, @extract/extract!@, @reverse_merge/reverse_merge!@, @assert_valid_keys@. 45 | 46 | * Configliere::Params @#defaults@, @#resolve!@, @#validate!@ and @#use@ all return self, so you can say 47 | 48 |
Settings.defaults(:hi => :mom).use(:commandline).resolve!.validate!
49 | 50 | * Configliere::Params.use now adds middleware only to the *instance* -- you don't get commandline params in your settings object just becaus some other class use'd it. 51 | * Configliere::Params middlewares should supply a block to Configliere::Params.on_use to #extend the object, #use related middlewares, etc. 52 | 53 | h4. Cleanup of Configliere::Define. 54 | 55 | * The prepositional soup of accessor sugar is gone, replaced by three public methods: 56 | - @definition_of(param, aspect=nil)@ (without arg, gives the definition hash; with arg, gives that value); 57 | - @params_with(aspect)@ (hash of param => aspect_definition) 58 | - has_definition?(param) (has #define been called for that param?) 59 | see below for what's gone. 60 | * Specs for the magical getter/setter given when you define a param, and for deep key handling. 61 | 62 | * Commandline now tracks commandline arguments that haven't been define'd in @unknown_argvs@. It adopts them all the same, but if you don't like what you see there you're free to raise a warning or error. 63 | * Single-character flags now take an argument: @-a=hello@ or @-a hello@ 64 | 65 | h4. Misc 66 | 67 | * :encrypted keys are now stored as base64-encoded 68 | * cleaned up handling of encrypt_pass -- it's no longer publicly readable; set it as a member (Settings[:encrypt_pass]) or through the ENCRYPT_PASS environment variable and it will be adopted in the course of action (and, if a member, deleted). Because of the #use method refactoring, you can have independent settings bundles use encrypted independently. 69 | * Prompt is now its own middleware: 70 | 71 |
 72 |   Settings.use :prompt
 73 |   pwd = Settings.prompt_for(:password)
 74 | 
75 | 76 | * bin/configliere shows off the git-style-binaries aspect, and helps you set encrypted params. 77 | * Specs documentation is now quite readable 78 | * Cleaned up the STDERR-capturing part of the specs 79 | * Added spork and watchr support to the specs. 80 | 81 | h4. Killing features for great justice: 82 | 83 | * No modifications to core classes. Scripts that were secretly depending on Configliere for blank? etc might now break. deep_merge, deep_set and deep_delete have been moved to a DeepHash class, and the Sash class is gone. 84 | 85 | * dashed commandline params are accepted but cause a warning -- they do *not* serve as deep keys. By default Configliere happily accepts them. To change that, make a middleware to either convert --foo-bar to --foo.bar, or convert --foo-bar to --foo_bar. 86 | 87 | * config_file now just takes a filename: Instead of a magic handle, scope a segment of the file with the :env option: 88 | 89 |
Settings.read('./config/foo.yaml', :env => ENV['RACK_ENV'])
90 | 91 | Stripped out a whole raft of oversweet sugar: 92 | 93 | * Settings.argv Settings.rest 94 | * Settings.commands? Settings.params_with(:command).empty? 95 | * Configliere.new Configliere::Param.new 96 | * param_or_ask Settings.prompt_for (with Settings.use(:prompt)) 97 | * param_definitions Not publicly accessible, use definition_of(param) 98 | * described_params Settings.params_with(:description) 99 | * type_for Settings.definition_of(param, :type) 100 | * typed_params Settings.params_with(:type) 101 | * required_params Settings.params_with(:required) 102 | * define with :no_help and :no_env_help (instead say @:internal@) 103 | 104 | * Removed the long-deprecated :git_style_binaries synonym for :commands 105 | 106 | h2. Version 0.3.4 107 | 108 | The jump in minor version number was unintentional. 109 | 110 | * handle case wehre file is empty on environment merge 111 | * read returns self, so can chain 112 | 113 | h2. Version 0.2.3 114 | 115 | * Added a feature to load only production/development/etc subhash from a config file, so: 116 | 117 |
118 |   Settings.read(root_path('config/foo.yaml'), :env => ENV['RACK_ENV'])
119 | 
120 | 121 | h2. Version 0.2.1 2011-01-28 122 | 123 | * Missing required params include their definition in error message 124 | * finally{} blocks don't supply a parameter any more -- use self instead. 125 | 126 | h2. Version 0.1.1 2010-08-17 127 | 128 | * Settings.read now does expand_path on the file path 129 | 130 | h2. Version 0.1.0 2010-07-24 131 | 132 | * Version 0.1 !!! 133 | * Single-letter option flags 134 | * Can give a ':finally' proc (or hand a block to) Settings.define. Example: 135 | 136 |
137 |     Settings.define :key_pair_file,        :description => 'AWS Key pair file', :finally => lambda{ Settings.key_pair_file = File.expand_path(Settings.key_pair_file.to_s) if Settings.key_pair_file }
138 |     Settings.define :key_pair,             :description => "AWS Key pair name. If not specified, it's taken from key_pair_file's basename", :finally => lambda{ Settings.key_pair ||= File.basename(Settings.key_pair_file.to_s, '.pem') if Settings.key_pair_file }
139 | 
140 | 141 | h2. Version 0.0.8 2010-05-02 142 | 143 | * Provisional implementation of git-style binaries (foo-ls and foo-chmod and so on) 144 | * Minor fixes 145 | 146 | h2. Version 0.0.6 2010-04-05 147 | 148 | * configliere/define and configliere/config_file are included by default. 149 | * fixed a bug when ENV['HOME'] is missing (running as root) 150 | 151 | h2. Version 0.0.5 2010-01-27 152 | 153 | Configliere.use will load all gems by default 154 | 155 | h2. Version 0.0.4 2010-01-16 156 | 157 | * Cured a variety of issues noted by "@monad":http://github.com/monad -- thanks for the feedback! 158 | 159 | h2. Version 0.0.3 2010-01-15 160 | 161 | * @Settings.param@ now only works for params that have been @#define@'d : 162 | 163 |
164 |     Settings :no_yuo => 'oops'
165 |     Settings.no_yuo
166 |     #=> NoMethodError: undefined method `no_yuo' for { :no_yuo => "oops" } :Configliere::Param
167 |     Settings.define :happy_param, :default => 'yay'
168 |     Settings.happy_param
169 |     #=> "yay" 
170 | 
171 | 172 | * Note that you *must* use symbols as keys (except for dotted notation for deep keys). See the README. 173 | * You must now define environment variables using @Settings.env_vars :param => 'ENV_VAR'@. The order was switched to consistently use param as the key. Also, @environment@ was renamed to @env_var@ to avoid confusion with the "development/test/production" convention from rails and friends. 174 | * die takes an error code as option 175 | * Added example scripts for encrypted and config_block scripts 176 | * The directory path to a config_file will now be created automatically 177 | -------------------------------------------------------------------------------- /FEATURES.txt: -------------------------------------------------------------------------------- 1 | 2 | Configliere::Commandline 3 | with long-format argvs 4 | accepts --param=val pairs 5 | accepts --dotted.param.name=val pairs as deep keys 6 | NO LONGER accepts --dashed-param-name=val pairs as deep keys 7 | adopts only the last-seen of duplicate commandline flags 8 | does NOT set a bare parameter (no "=") followed by a non-param to that value 9 | sets a bare parameter (no "=") to true 10 | sets an explicit blank to nil 11 | captures non --param args into Settings.rest 12 | stops processing args on "--" 13 | places undefined argvs into #unknown_argvs 14 | with single-letter flags 15 | accepts them separately 16 | accepts them as a group ("-abc") 17 | accepts a value with -d=new_val 18 | accepts a space-separated value (-d new_val) 19 | accepts a space-separated value only if the next arg is not a flag 20 | stores unknown single-letter flags in unknown_argvs 21 | the help message 22 | displays help 23 | displays the single-letter flags 24 | displays command line options 25 | lets me die 26 | recycling a commandline 27 | exports dashed flags 28 | #resolve! 29 | calls super and returns self 30 | #validate! 31 | calls super and returns self 32 | 33 | Configliere::Commands 34 | when no commands are defined 35 | should know that no commands are defined 36 | should not shift the ARGV when resolving 37 | should still recognize a git-style-binary command 38 | a simple command 39 | should continue to parse flags when the command is given 40 | should continue to set args when the command is given 41 | should recognize the command when given 42 | should recognize when the command is not given 43 | a complex command 44 | should still recognize the outer param and the args 45 | should recognize the inner param 46 | the help message 47 | displays a modified usage 48 | displays the commands and their descriptions 49 | #resolve! 50 | calls super and returns self 51 | #validate! 52 | calls super and returns self 53 | 54 | Configliere::ConfigBlock 55 | resolving 56 | runs blocks 57 | resolves blocks last 58 | calls super and returns self 59 | #validate! 60 | calls super and returns self 61 | 62 | Configliere::ConfigFile 63 | is used by default 64 | loading a config file 65 | no longer provides a default config file 66 | warns but does not fail if the file is missing 67 | successfully 68 | with an absolute pathname uses it directly 69 | with a simple filename, references it to the default config dir 70 | returns the config object for chaining 71 | #read_yaml 72 | loads yaml 73 | with an environment scope 74 | slices out a subhash given by :env 75 | slices out a different subhash with a different :env 76 | does no slicing without the :env option 77 | has no effect if the key given by :env option is absent 78 | lets you use a string if the loading hash has a string 79 | saves to a config file 80 | with an absolute pathname, as given 81 | with a simple pathname, in the default config dir 82 | and ensures the directory exists 83 | #resolve! 84 | calls super and returns self 85 | #validate! 86 | calls super and returns self 87 | 88 | Crypter 89 | encrypts 90 | decrypts 91 | 92 | DeepHash 93 | responds to #symbolize_keys, #symbolize_keys! and #stringify_keys but not #stringify_keys! 94 | #initialize 95 | adopts a Hash when given 96 | converts all pure Hash values into DeepHashes if param is a Hash 97 | does not convert Hash subclass values into DeepHashes 98 | converts all value items if value is an Array 99 | delegates to superclass constructor if param is not a Hash 100 | #update 101 | converts all keys into symbols when param is a Hash 102 | converts all Hash values into DeepHashes if param is a Hash 103 | #[]= 104 | symbolizes keys 105 | deep-sets dotted vals, replacing values 106 | deep-sets dotted vals, creating new values 107 | deep-sets dotted vals, auto-vivifying intermediate hashes 108 | converts all Hash value into DeepHash 109 | #[] 110 | deep-gets dotted vals 111 | #to_hash 112 | returns instance of Hash 113 | preserves keys 114 | preserves value 115 | #compact 116 | removes nils but not empties or falsehoods 117 | leaves original alone 118 | #compact! 119 | removes nils but not empties or falsehoods 120 | modifies in-place 121 | #slice 122 | returns a new hash with only the given keys 123 | with bang replaces the hash with only the given keys 124 | ignores an array key 125 | with bang ignores an array key 126 | uses splatted keys individually 127 | with bank uses splatted keys individually 128 | #extract 129 | replaces the hash with only the given keys 130 | leaves the hash empty if all keys are gone 131 | gets values for all given keys even if missing 132 | is OK when empty 133 | returns an instance of the same class 134 | #delete 135 | converts Symbol key into String before deleting 136 | works with String keys as well 137 | #fetch 138 | converts key before fetching 139 | returns alternative value if key lookup fails 140 | #values_at 141 | is indifferent to whether keys are strings or symbols 142 | #symbolize_keys 143 | returns a dup of itself 144 | #symbolize_keys! 145 | with bang returns the deep_hash itself 146 | #stringify_keys 147 | converts keys that are all symbols 148 | returns a Hash, not a DeepHash 149 | only stringifies and hashifies the top level 150 | #assert_valid_keys 151 | is true and does not raise when valid 152 | fails when invalid 153 | #merge 154 | merges given Hash 155 | returns a new instance 156 | returns instance of DeepHash 157 | converts all Hash values into DeepHashes 158 | converts string keys to symbol keys even if they occur deep in the given hash 159 | DOES merge values where given hash has nil value 160 | replaces child hashes, and does not merge them 161 | #merge! 162 | merges given Hash 163 | returns a new instance 164 | returns instance of DeepHash 165 | converts all Hash values into DeepHashes 166 | converts string keys to symbol keys even if they occur deep in the given hash 167 | DOES merge values where given hash has nil value 168 | replaces child hashes, and does not merge them 169 | #reverse_merge 170 | merges given Hash 171 | returns a new instance 172 | returns instance of DeepHash 173 | converts all Hash values into DeepHashes 174 | converts string keys to symbol keys even if they occur deep in the given hash 175 | DOES merge values where given hash has nil value 176 | replaces child hashes, and does not merge them 177 | #deep_merge! 178 | merges two subhashes when they share a key 179 | merges two subhashes when they share a symbolized key 180 | preserves values in the original 181 | converts all Hash values into DeepHashes 182 | converts string keys to symbol keys even if they occur deep in the given hash 183 | replaces values from the given hash 184 | replaces arrays and does not append to them 185 | does not replaces values where given hash has nil value 186 | #deep_set 187 | should set a new value (single arg) 188 | should set a new value (multiple args) 189 | should replace an existing value (single arg) 190 | should replace an existing value (multiple args) 191 | should auto-vivify intermediate hashes 192 | #deep_delete 193 | should remove the key from the array (multiple args) 194 | should remove the key from the array (multiple args) 195 | should return the value if present (single args) 196 | should return the value if present (multiple args) 197 | should return nil if the key is absent (single arg) 198 | should return nil if the key is absent (multiple args) 199 | 200 | Configliere::Define 201 | takes a description 202 | defining any aspect of a param 203 | adopts values 204 | returns self 205 | merges new definitions 206 | lists params defined as the given aspect 207 | definition_of 208 | with a param, gives me the description hash 209 | with a param and attr, gives me the description hash 210 | symbolizes the param 211 | has_definition? 212 | is true if defined (one arg) 213 | is false if not defined (one arg) 214 | is true if defined and attribute is defined 215 | is false if defined and attribute is defined 216 | is false if not defined and attribute is given 217 | type coercion 218 | for boolean converts "0" to false 219 | for boolean converts 0 to false 220 | for boolean converts "" to false 221 | for boolean converts [] to true 222 | for boolean converts nil to nil 223 | for boolean converts "1" to true 224 | for boolean converts 1 to true 225 | for boolean converts "5" to true 226 | for boolean converts "true" to true 227 | for Integer converts "5" to 5 228 | for Integer converts 5 to 5 229 | for Integer converts nil to nil 230 | for Integer converts "" to nil 231 | for Integer converts "5" to 5 232 | for Integer converts 5 to 5 233 | for Integer converts nil to nil 234 | for Integer converts "" to nil 235 | for Float converts "5.2" to 5.2 236 | for Float converts 5.2 to 5.2 237 | for Float converts nil to nil 238 | for Float converts "" to nil 239 | for Symbol converts "foo" to :foo 240 | for Symbol converts :foo to :foo 241 | for Symbol converts nil to nil 242 | for Symbol converts "" to nil 243 | for Date converts "1985-11-05" to # 244 | for Date converts nil to nil 245 | for Date converts "" to nil 246 | for Date converts "blah" to nil 247 | for DateTime converts "1985-11-05 11:00:00" to # 248 | for DateTime converts nil to nil 249 | for DateTime converts "" to nil 250 | for DateTime converts "blah" to nil 251 | for Array converts ["this", "that", "thother"] to ["this", "that", "thother"] 252 | for Array converts "this,that,thother" to ["this", "that", "thother"] 253 | for Array converts "alone" to ["alone"] 254 | for Array converts "" to [] 255 | for Array converts nil to nil 256 | for other converts "5" to "5" 257 | for other converts 5 to 5 258 | for other converts nil to nil 259 | for other converts "" to nil 260 | converts :now to the current moment 261 | creates magical methods 262 | answers to a getter if the param is defined 263 | answers to a setter if the param is defined 264 | does not answer to a getter if the param is not defined 265 | does not answer to a setter if the param is not defined 266 | defining requireds 267 | lists required params 268 | counts false values as present 269 | counts nil-but-set values as missing 270 | counts never-set values as missing 271 | lists all missing values when it raises 272 | defining deep keys 273 | allows required params 274 | allows flags 275 | type converts 276 | #resolve! 277 | calls super and returns self 278 | #validate! 279 | calls super and returns self 280 | 281 | Configliere::Encrypted 282 | defines encrypted params 283 | with :encrypted => true 284 | but not if :encrypted => false 285 | only if :encrypted is given 286 | the encrypt_pass 287 | will take an environment variable if any exists 288 | will take an internal value if given, and remove it 289 | encrypts 290 | all params with :encrypted 291 | fails unless encrypt_pass is set 292 | decrypts 293 | all params marked encrypted 294 | loading a file 295 | encrypts 296 | decrypts 297 | #resolve! 298 | calls super and returns self 299 | removes the encrypt_pass from sight 300 | #validate! 301 | calls super and returns self 302 | 303 | Configliere::EnvVar 304 | environment variables can be defined 305 | with #env_vars, a simple value like "HOME" uses the corresponding key :home 306 | with #env_vars, a hash pairs environment variables into the individual params 307 | with #define 308 | #resolve! 309 | calls super and returns self 310 | #validate! 311 | calls super and returns self 312 | 313 | Configliere::Param 314 | calling #defaults 315 | deep_merges new params 316 | returns self, to allow chaining 317 | adding plugins with #use 318 | requires the corresponding library 319 | returns self, to allow chaining 320 | invokes the on_use handler 321 | #resolve! 322 | calls super and returns self 323 | 324 | Configliere::Prompt 325 | when the value is already set, #prompt_for 326 | returns the value 327 | returns the value even if nil 328 | returns the value even if nil 329 | when prompting, #prompt_for 330 | prompts for a value if missing 331 | uses an explicit hint 332 | uses the description as hint if none given 333 | #resolve! 334 | calls super and returns self 335 | 336 | Configliere 337 | creates a global variable Settings, for universality 338 | creates a global method Settings, so you can say Settings(:foo => :bar) 339 | requires corresponding plugins when you call use 340 | 341 | Finished in 0.61948 seconds 342 | 247 examples, 0 failures 343 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | 5 | # Only necessary if you want to use Configliere::Prompt 6 | gem 'jruby-openssl', :platform => [:jruby] if RUBY_PLATFORM =~ /java/ 7 | 8 | group :docs do 9 | gem 'RedCloth', ">= 4.2", :require => "redcloth" 10 | gem 'redcarpet', ">= 2.1", :platform => [:ruby] 11 | gem 'kramdown', :platform => [:jruby] 12 | end 13 | 14 | # Gems for testing and coverage 15 | group :test do 16 | gem 'simplecov', ">= 0.5", :platform => [:ruby_19], :require => false 17 | gem 'json' 18 | end 19 | 20 | # Gems you would use if hacking on this gem (rather than with it) 21 | group :support do 22 | gem 'pry' 23 | # 24 | gem 'guard', ">= 1.0", :platform => [:ruby_19] 25 | gem 'guard-rspec', ">= 0.6", :platform => [:ruby_19] 26 | gem 'guard-yard', :platform => [:ruby_19] 27 | if RUBY_PLATFORM.include?('darwin') 28 | gem 'rb-fsevent', ">= 0.9", :platform => [:ruby_19] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | format = 'progress' # 'doc' for more verbose, 'progress' for less 4 | tags = %w[ ] # builder_spec model_spec 5 | 6 | guard 'rspec', :version => 2, :cli => "--format #{format} #{ tags.map{|tag| "--tag #{tag}"}.join(' ') }" do 7 | watch(%r{^spec/.+_spec\.rb$}) 8 | watch(%r{^examples/(\w+)/(.+)\.rb$}) { |m| "spec/examples/#{m[1]}_spec.rb" } 9 | watch(%r{^examples/(\w+)\.rb$}) { |m| "spec/examples/#{m[1]}_spec.rb" } 10 | watch(%r{^lib/(.+)/(.+)\.rb$}) { |m| "spec/#{m[1]}/#{m[2]}_spec.rb" } 11 | watch(%r{^lib/(\w+)\.rb$}) { |m| "spec/#{m[1]}}_spec.rb" } 12 | watch('spec/spec_helper.rb') { 'spec' } 13 | watch(/spec\/support\/(.+)\.rb/) { 'spec' } 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License for Configliere 2 | 3 | The configliere code is __Copyright (c) 2011, 2012 Infochimps, Inc__ 4 | 5 | This code is 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 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | 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. 10 | 11 | __________________________________________________________________________ 12 | 13 | # Apache License 14 | 15 | 16 | Apache License 17 | Version 2.0, January 2004 18 | http://www.apache.org/licenses/ 19 | 20 | _TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION_ 21 | 22 | ## 1. Definitions. 23 | 24 | * **License** shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 25 | 26 | * **Licensor** shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 27 | 28 | * **Legal Entity** shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 29 | 30 | * **You** (or **Your**) shall mean an individual or Legal Entity exercising permissions granted by this License. 31 | 32 | * **Source** form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 33 | 34 | * **Object** form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 35 | 36 | * **Work** shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 37 | 38 | * **Derivative Works** shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 39 | 40 | * **Contribution** shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 41 | 42 | * **Contributor** shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 43 | 44 | ## 2. Grant of Copyright License. 45 | 46 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 47 | 48 | ## 3. Grant of Patent License. 49 | 50 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 51 | 52 | ## 4. Redistribution. 53 | 54 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 55 | 56 | - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 57 | - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 58 | - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 59 | - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 60 | 61 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 62 | 63 | ## 5. Submission of Contributions. 64 | 65 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 66 | 67 | ## 6. Trademarks. 68 | 69 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 70 | 71 | ## 7. Disclaimer of Warranty. 72 | 73 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 74 | 75 | ## 8. Limitation of Liability. 76 | 77 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 78 | 79 | ## 9. Accepting Warranty or Additional Liability. 80 | 81 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 82 | 83 | _END OF TERMS AND CONDITIONS_ 84 | 85 | ## APPENDIX: How to apply the Apache License to your work. 86 | 87 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets `[]` replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 88 | 89 | > Copyright [yyyy] [name of copyright owner] 90 | > 91 | > 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 92 | > 93 | > http://www.apache.org/licenses/LICENSE-2.0 94 | > 95 | > 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. 96 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. Configliere 2 | 3 | This repository has moved: "infodhimps-platform/configliere":https://github.com/infochimps-platform/configliere 4 | All further code work and gems will take place in the above repo, not here. 5 | 6 | Configliere provides discreet configuration for ruby scripts. 7 | 8 | bq. So, Consigliere of mine, I think you should tell your Don what everyone knows. -- Don Corleone 9 | 10 | You've got a script. It's got some settings. Some settings are for this module, some are for that module. Most of them don't change. Except on your laptop, where the paths are different. Or when you're in production mode. Or when you're testing from the command line. 11 | 12 | Configliere manages settings from many sources: static constants, simple config files, environment variables, commandline options, straight ruby. You don't have to predefine anything, but you can ask configliere to type-convert, require, document or password-obscure any of its fields. Basically: *Settings go in, the right thing happens*. 13 | 14 | !https://secure.travis-ci.org/mrflip/configliere.png?branch=master(Build Status)!:http://travis-ci.org/mrflip/configliere 15 | 16 | h2. Example 17 | 18 | Here's a simple example, using params from a config file and the command line. In the script: 19 | 20 |
 21 |     #/usr/bin/env ruby
 22 |     require 'configliere'
 23 |     Settings.use :commandline
 24 | 
 25 |     # Supply defaults inline.
 26 |     Settings({
 27 |       :dest_time => '11-05-1955',
 28 |       :delorean => {
 29 |         :power_source => 'plutonium',
 30 |         :roads_needed => true,
 31 |         },
 32 |       :username => 'marty',
 33 |     })
 34 |     
 35 |     # Pre-defining params isn't required, but it's easy and expressive to do so:
 36 |     Settings.define :dest_time, :type => DateTime, :description => "Target date"
 37 |     # This defines a 'deep key': it controls Settings[:delorean][:roads_needed]
 38 |     Settings.define 'delorean.roads_needed', :type => :boolean
 39 | 
 40 |     # The settings in this file will be merged with the above
 41 |     Settings.read './examples/simple_script.yaml'
 42 | 
 43 |     # resolve! the settings: load the commandline, do type conversion, etc.
 44 |     Settings.resolve!
 45 |     p Settings
 46 | 
47 | 48 | We'll override some of the defaults with a config file, in this case ./examples/simple_script.yaml 49 | 50 |
 51 |     # Settings for return
 52 |     :dest_time:       11-05-1985
 53 |     :delorean:    
 54 |       :power_source:  1.21 jiggawatts
 55 | 
56 | 57 | Output, when run with commandline parameters as shown: 58 | 59 |
 60 |     ./time_machine.rb --username=doc_brown --delorean.roads_needed="" --delorean.power_source="Mr. Fusion"
 61 | 
 62 |     {:delorean => {:power_source=>"Mr. Fusion", :roads_needed=>nil}, :username=>"doc_brown", :dest_time=>#}
 63 | 
64 | 65 | For an extensive usage in production, see the "wukong gem.":http://github.com/mrflip/wukong 66 | 67 | h2. Notice 68 | 69 | Configliere 4.x now has 100% spec coverage, more powerful commandline handling, zero required dependencies. However, it also strips out several obscure features and much magical code, which breaks said obscure features and magic-dependent code. See the "CHANGELOG.":CHANGELOG.textile for details as you upgrade. 70 | 71 | h2. Design goals: 72 | 73 | * *Omerta (Code of Silence)*. Most commandline parsers force you to pre-define all your parameters in a centralized and wordy syntax. In configliere, you don't have to pre-define anything -- commandline parameters map directly to values in the Configliere hash. Here's all you need to have full-fledged commandline params: 74 | 75 |
 76 |   $ cat ./shorty.rb
 77 |   require 'configliere'
 78 |   Settings.use(:commandline).resolve!
 79 |   p [Settings, Settings.rest]
 80 |   
 81 |   $ ./shorty.rb --foo=bar go
 82 |   [{:foo=>"bar"}, ["go"]]
 83 | 
84 | 85 | * *Be willing to sit down with the Five Families*. Takes settings from (at your option): 86 | ** Pre-defined defaults from constants 87 | ** Simple config files 88 | ** Environment variables 89 | ** Commandline options and git-style command runners 90 | ** Ruby block (called when all other options are in place) 91 | 92 | * *Don't go outside the family*. Has no dependencies and requires almost no code in your script. Configliere makes no changes to standard ruby classes. 93 | 94 | * *Offer discreet counsel*. Configliere offers many features, but only loads the code you request explicitly by calling @use@. 95 | 96 | * *Don't mess with my crew*. Settings for a model over here can be done independently of settings for a model over there, and don't require asking the boss to set something up. You centralize configuration _values_ while distributing configuration _definition_: 97 | 98 |
 99 |     # In lib/handler/mysql.rb
100 |     Settings.define :mysql_host, :type => String, :description => "MySQL db hostname", :default => 'localhost'
101 | 
102 |     # In app/routes/homepage.rb
103 |     Settings.define :background_color, :description => "Homepage background color"
104 | 
105 |     # In config/app.yaml
106 |     ---
107 |     :background_color:  '#eee'
108 |     :mysql_host:        'brains.infochimps.com'
109 | 
110 | 111 | You can decentralize even more by giving modules their own config files or separate Configliere::Param objects. 112 | 113 | * *Can hide your assets*. Rather than storing passwords and API keys in plain sight, configliere has a protection racket that can obscure values when stored to disk. 114 | 115 | fuhgeddaboudit. 116 | 117 | 118 | h2. Settings structure 119 | 120 | A Configliere settings object is just a (mostly-)normal hash. 121 | 122 | You can define static defaults in your module 123 | 124 |
125 |     Settings({
126 |       :dest_time => '11-05-1955',
127 |       :fluxcapacitor => {
128 |         :speed => 88,
129 |         },
130 |       :delorean => {
131 |         :power_source => 'plutonium',
132 |         :roads_needed => true,
133 |         },
134 |       :username => 'marty',
135 |       :password => '',
136 |     })
137 | 
138 | 139 | All simple keys should be symbols. Retrieve the settings as: 140 | 141 |
142 |     # hash keys
143 |     Settings[:dest_time]                 #=> '11-05-1955'
144 |     # deep keys
145 |     Settings[:delorean][:power_source]   #=> 'plutonium'
146 |     Settings[:delorean][:missing]        #=> nil
147 |     Settings[:delorean][:missing][:fail] #=> raises an error
148 |     # dotted keys resolve to deep keys
149 |     Settings['delorean.power_source']    #=> 'plutonium'
150 |     Settings['delorean.missing']         #=> nil
151 |     Settings['delorean.missing.fail']    #=> nil
152 |     # method-like (no deep keys tho, and you have to #define the param; see below)
153 |     Settings.dest_time                   #=> '11-05-1955'
154 | 
155 | 156 | h2. Configuration files 157 | 158 | Call @Settings.read(filename)@ to read a YAML config file. 159 | 160 |
161 |     # Settings for version II.
162 |     :dest_time:        11-05-2015
163 |     :delorean:
164 |       :power_source:    Mr. Fusion
165 |       :roads_needed:    ~
166 | 
167 | 168 | If a bare filename (no '/') is given, configliere looks for the file in @Configliere::DEFAULT_CONFIG_DIR@ (normally ~/.configliere). Otherwise it loads the given file. 169 | 170 |
171 |     Settings.read('/etc/time_machine.yaml')  # looks in /etc/time_machine.yaml
172 |     Settings.read('time_machine.yaml')       # looks in ~/.configliere/time_machine.yaml
173 | 
174 | 175 | As you can see, you're free to use as many config files as you like. Loading a config file sets values immediately, so later-loaded files win out over earlier-loaded ones. 176 | 177 | You can save configuration too: 178 | 179 |
180 |     Settings.save!('/etc/time_machine.yaml') # overwrites /etc/time_machine.yaml
181 |     Settings.save!('time_machine.yaml')      # overwrites ~/.configliere/time_machine.yaml
182 | 
183 | 184 | h2. Command-line parameters 185 | 186 |
187 |     # Head back
188 |     time_machine --delorean.power_source='1.21 jiggawatt lightning strike' --dest_time=11-05-1985
189 |     # (in the time_machine script:)
190 |     Settings.use(:commandline)
191 |     Settings.resolve!
192 | 
193 | 194 | Interpretation of command-line parameters: 195 | 196 | * *name-val params*: @--param=val@ sets @Configliere[:param]@ to val. You _must_ use the '=' in there: ./my_cmd --filename=bar good, ./my_cmd --filename bar bad. 197 | * *boolean params*: @--param@ sets @Configliere[:param]@ to be true. @--param=""@ sets @Configliere[:param]@ to be nil. 198 | * *single-char flags*: Define a flag for a variable: Settings.define :filename, :flag => "f" allows you to say ./my_cmd -f=bar. 199 | * *scoped params*: A dot within a parameter name scopes that parameter: @--group.sub_group.param=val@ sets @Configliere[:group][:subgroup][:param]@ to val (and similarly for boolean parameters). 200 | ** Only @[\w\.]+@ are accepted in parameter names. '-' is currently accepted but causes a warning. 201 | * *Settings.rest*: anything else is stored, in order, in @Settings.rest@. 202 | * *stop marker*: a @--@ alone stops parameter processing and tosses all remaining params (not including the @--@) into Settings.rest. 203 | 204 | Here are some things you don't get: 205 | 206 | * Configliere doesn't complain about un-@define@'d commandline argvs. However, it does store each undefine'd argv (when resolve! is called) into @unknown_argvs@, so you can decide what to do about it. 207 | * Apart from converting @''@ (an explicit blank string) to @nil@, no type coercion is performed on parameters unless requested explicitly (see below). 208 | * No validation is performed on parameters, but you can insert a middleware with a @validate!()@ method, or use a @:finally@ block. 209 | * No ordering or multiplicity is preserved: you can't say @--file=this --file=that@. Instead, define the param as an array Settings.define :file, :type => Array and give a simple comma-separated list. 210 | 211 | Commandline parameters are demonstrated in "examples/simple_script.rb":http://github.com/mrflip/configliere/tree/master/examples/simple_script.rb and "examples/env_var_script.rb":http://github.com/mrflip/configliere/tree/master/examples/env_var_script.rb 212 | 213 | h2. Defined Parameters 214 | 215 | You don't have to pre-define parameters, but you can: 216 | 217 |
218 |     Settings.define :dest_time, :type => DateTime, :description => 'Arrival time'
219 |     Settings.define 'delorean.power_source', :env_var => 'POWER_SOURCE', :description => 'Delorean subsytem supplying power to the Flux Capacitor.'
220 |     Settings.define :password, :required => true, :encrypted => true
221 | 
222 | 223 | * @:description@: documents a param. 224 | * @:type@: converts params to a desired form. 225 | * @:required@: marks params required. 226 | * @:encrypted@: marks params to be obscured when saved to disk. See [#Encrypted Parameters] below for caveats. 227 | * @:env_var@: take param from given environment variable if set. 228 | 229 | Defined parameters are demonstrated in most of the "example scripts":http://github.com/mrflip/configliere/tree/master/examples 230 | 231 | h3. Description 232 | 233 | If you define a param's description, besides nicely documenting it within your code the description will be stuffed into the output when the --help commandline option is invoked. 234 | 235 |
236 |   $ ./examples/simple_script
237 |   usage: simple_script.rb [...--param=val...]
238 | 
239 |   Params:
240 |     --delorean.roads_needed   delorean.roads_needed
241 |     --dest_time=DateTime      Date to travel to [Default: 11-05-1955]
242 | 
243 | 244 | h3. Type Conversion 245 | 246 | Parameters defined with a @:type@ and will be type-converted when you call @Settings.resolve!@. 247 | 248 |
249 |     Settings.define :dest_time,     :type => DateTime
250 |     Settings.define :fugeddaboudit, :type => Array
251 |     Settings :fugeddaboudit => 'badabing,badaboom,hey', :dest_time => '11-05-1955'
252 |     Settings.resolve!
253 |     Settings[:fugeddaboudit]   #=> ['badabing', 'badaboom', 'hey']
254 |     Settings[:dest_time]       #=> #
255 | 
256 | 257 | Configliere can coerce parameter values to Integer, Float, :boolean, Symbol, Array, Date and DateTime. 258 | 259 | * :boolean converts nil to nil ; false, 'false', 0, '0' and '' to false; and everything else to true. 260 | * Array just does a simple split on ",". It doesn't do any escaping or quoting. 261 | * Date and DateTime convert unparseable inputs to nil. 262 | * :filename calls File.expand_path() on the param. 263 | 264 | h3. Required Parameters 265 | 266 | Any required parameter found to be nil raise an error (listing all missing params) when you call Settings.resolve! (See "examples/env_var_script.rb":http://github.com/mrflip/configliere/tree/master/examples/env_var_script.rb) 267 | 268 | h3. Environment Variables 269 | 270 |
271 |     Settings.define :dest_time,   :env_var => 'DEST_TIME'
272 |     Settings.define :environment, :env_var => 'RACK_ENV'
273 | 
274 | 275 | h3. Encrypted Parameters 276 | 277 | Define a param to be encrypted and invoke Settings.save!. It will use Settings.encrypt_pass (or the ENCRYPT_PASS environment variable) to encrypt the data when it is saved to disk. (see "examples/encrypted_script.rb":http://github.com/mrflip/configliere/tree/master/examples/encrypted_script.rb) 278 | 279 |
280 |     Settings.use :encrypted
281 |     Settings.define 'amazon.api.key', :encrypted => true
282 |     Settings 'amazon.api.key' => 'fnord'
283 | 
284 | 285 | In this example, the hash saved to disk will contain @{ :amazon => { :api => { :encrypted_key => "...encrypted val..." } } }@. After reading from disk, #resolve! will recover its original value: @{ :amazon => { :api => { :key => "fnord" } } }@. 286 | 287 | bq. There are two kinds of cryptography in this world: cryptography that will stop your kid sister from reading your files, and cryptography that will stop major governments from reading your files. This book is about the latter. -- Preface to Applied Cryptography by Bruce Schneier 288 | 289 | Configliere provides the former. 290 | 291 | Anyone with access to the script, its config files and its normal launch environment can recover the plaintext password; but it at least doesn't appear when you cat the file while giving a presentation. 292 | 293 | h2. Ruby Block 294 | 295 |
296 |     Settings.use :config_block
297 |     Settings.finally do |c|
298 |       c.dest_time = (Time.now + 60) if c.username == 'einstein'
299 |       # you can use hash syntax too
300 |       c[:dest_time] = (Time.now + 60) if c[:username] == 'einstein'
301 |     end
302 |     #
303 |     # ... rest of setup ...
304 |     #
305 |     Settings.resolve!    # the finally blocks will be called in order
306 | 
307 | 308 | Configliere 'finally' blocks are invoked when you call @resolve!@. They're guaranteed to be called at the end of the resolve chain, and before the validate chain. 309 | 310 | Config blocks are demonstrated in "examples/config_block.rb":http://github.com/mrflip/configliere/tree/master/examples/config_block.rb 311 | 312 | h2. Shortcut syntax for deep keys 313 | 314 | You can use a 'dotted key' like 'delorean.power_source' as simple notation for a deep key: @Settings['delorean.power_source']@ is equivalent to @Settings[:delorean][:power_source]@. You can use a dotted key in any simple reference: 315 | 316 |
317 |   Settings['delorean.power_source'] = "Mr. Fusion"
318 |   Settings[:delorean][:power_source]
319 |   #=> "Mr. Fusion"
320 |   Settings.delete('delorean.power_source')
321 |   #=> "Mr. Fusion"
322 |   Settings
323 |   #=> { :delorean => {} }
324 | 
325 | 326 | Intermediate keys "auto-vivify" (automatically create any intervening hashes): 327 | 328 |
329 |   Settings['one.two.three'] = "To tha Fo'"
330 |   # Settings is { :one => { :two => { :three => "To tha Fo'" } }, :delorean => { :power_source => "Mr. Fusion" }
331 | 
332 | 333 | h2. Independent Settings 334 | 335 | All of the above examples use the global variable @Settings@, defined in configliere.rb. It really works fine in practice, even where several systems intersect. You're free to define your own settings universe though: 336 | 337 |
338 |     class Wolfman
339 |       def config
340 |         @config ||= Configliere::Param.new.use(:commandline).defaults({
341 |           :moon    => 'full',
342 |           :nards   => true,
343 |           })
344 |       end
345 |     end
346 | 
347 |     teen_wolf = Wolfman.new
348 |     teen_wolf.config.defaults(:give_me => 'keg of beer')
349 |     
350 |     teen_wolf.config #=> {:moon=>"full", :nards=>true, :give_me=>"keg of beer" }
351 |     Settings         #=> {}
352 | 
353 | 354 | Values in here don't overlap with the Settings object or any other settings universe. However, every one that pulls in commandline params gets a full copy of the commandline params. 355 | 356 | h2. Project info 357 | 358 | h3. Note on Patches/Pull Requests 359 | 360 | * Fork the project. 361 | * Make your feature addition or bug fix. 362 | * Add tests for it. This is important so I don't break it in a future version unintentionally. 363 | * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 364 | * Send a pull request to github.com/mrflip 365 | * Drop a line to the mailing list for infochimps open-source projects, infochimps-code@googlegroups.com 366 | 367 | h3. Copyright 368 | 369 | Copyright (c) 2010 mrflip. See LICENSE for details. 370 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' unless defined?(Gem) 2 | require 'bundler/setup' 3 | Bundler.setup(:default, :development) 4 | require 'rake' 5 | 6 | task :default => :rspec 7 | 8 | require 'rspec/core/rake_task' 9 | RSpec::Core::RakeTask.new(:rspec) do |spec| 10 | Bundler.setup(:default, :development, :test) 11 | spec.pattern = 'spec/**/*_spec.rb' 12 | end 13 | 14 | desc "Run RSpec with code coverage" 15 | task :cov do 16 | ENV['CONFIGLIERE_COV'] = "yep" 17 | Rake::Task[:rspec].execute 18 | end 19 | 20 | require 'yard' 21 | YARD::Rake::YardocTask.new do 22 | Bundler.setup(:default, :development, :docs) 23 | end 24 | 25 | require 'jeweler' 26 | Jeweler::Tasks.new do |gem| 27 | Bundler.setup(:default, :development, :test) 28 | gem.name = 'configliere' 29 | gem.homepage = 'https://github.com/infochimps-labs/configliere' 30 | gem.license = 'Apache 2.0' 31 | gem.email = 'coders@infochimps.org' 32 | gem.authors = ['Infochimps'] 33 | 34 | gem.summary = %Q{Wise, discreet configuration management} 35 | gem.description = <<-EOF 36 | You\'ve got a script. It\'s got some settings. Some settings are for this module, some are for that module. Most of them don\'t change. Except on your laptop, where the paths are different. Or when you're in production mode. Or when you\'re testing from the command line. 37 | 38 | "" So, Consigliere of mine, I think you should tell your Don what everyone knows. "" -- Don Corleone 39 | 40 | Configliere manage settings from many sources: static constants, simple config files, environment variables, commandline options, straight ruby. You don't have to predefine anything, but you can ask configliere to type-convert, require, document or password-obscure any of its fields. Modules can define config settings independently of each other and the main program. 41 | EOF 42 | 43 | gem.executables = [] 44 | end 45 | Jeweler::RubygemsDotOrgTasks.new 46 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.4.19 -------------------------------------------------------------------------------- /bin/configliere: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.dirname(__FILE__)+'/../lib' 3 | require 'configliere' 4 | 5 | Settings.use :commands 6 | 7 | Settings.description = %Q{Client for the configliere gem: manipulate configuration and passwords for automated scripts} 8 | 9 | Settings.define_command :dump, :description => "Show the whole contents of the file" 10 | Settings.define_command :list, :description => "Show all params in the configliere file." 11 | Settings.define_command :get, :description => "show values given as commandline args, eg: #{File.basename($0)} get this that" 12 | Settings.define_command :set, :description => "sets values given as commandline args, eg: #{File.basename($0)} set this=1 that=3" 13 | Settings.define_command :delete, :description => "sets values given as commandline args, eg: #{File.basename($0)} delete this that" 14 | Settings.define_command :encrypt, :description => "encrypt the param" 15 | Settings.define_command :decrypt, :description => "Store the param as decrypted back into the file. Can be undone with 'encrypt'." 16 | 17 | Settings.define :from, :flag => 'f', :type => :filename, :description => "Configliere config file to load" 18 | Settings.define :into, :flag => 'i', :type => :filename, :description => "Configliere config file to save into" 19 | 20 | Settings.resolve! 21 | ARGV.replace [] 22 | 23 | Store = Configliere.new 24 | Store.read(Settings.from) if Settings.from 25 | 26 | # 27 | # Execute 28 | # 29 | 30 | case Settings.command_name 31 | when :dump 32 | puts Store.to_hash.to_yaml 33 | 34 | when :list 35 | $stderr.puts "Param names in #{Settings.from}:" 36 | Store.each do |attr, val| 37 | puts " #{attr}" 38 | end 39 | 40 | when :get 41 | $stderr.puts "Values for #{Settings.rest.join(", ")} from #{Settings.from}" 42 | Settings.rest.map(&:to_sym).each do |attr| 43 | puts "%-23s\t%s" % ["#{attr}:", Store[attr]] 44 | end 45 | 46 | when :set 47 | $stderr.puts "Setting #{Settings.rest.join(", ")} for #{Settings.from}" 48 | Settings.rest.each do |attr_val| 49 | attr, val = attr_val.split('=', 2) 50 | attr = attr.to_sym 51 | if val.nil? then warn "Please specify a value for #{attr}" ; next ; end 52 | Store[attr] = val 53 | 54 | puts "%-23s\t%s" % ["#{attr}:", val.inspect] 55 | end 56 | 57 | when :delete 58 | $stderr.puts "Deleting #{Settings.rest.join(", ")} from #{Settings.from}. O, I die, Horatio." 59 | Settings.rest.map(&:to_sym).each do |attr| 60 | Store.delete(attr) 61 | end 62 | 63 | when :encrypt 64 | Store.use :encrypted 65 | $stderr.puts "Encrypting #{Settings.rest.join(", ")} from #{Settings.from}. Fnord" 66 | Settings.rest.each{|attr| Store.define attr, :encrypted => true } 67 | Store.encrypt_pass = Settings.encrypt_pass || Settings[:encrypt_pass] 68 | Store.resolve! 69 | 70 | when :decrypt 71 | Store.use :encrypted 72 | $stderr.puts "Decrypting #{Settings.rest.join(", ")} from #{Settings.from}. Fnord" 73 | Settings.rest.each{|attr| Store.define attr, :encrypted => true } 74 | Store.encrypt_pass = Settings.encrypt_pass || Settings[:encrypt_pass] 75 | Store.resolve! 76 | 77 | puts Store.to_hash.to_yaml 78 | 79 | else 80 | Settings.die "Please use one of the given commands" 81 | end 82 | 83 | if Settings.into 84 | Store.save!(Settings.into) 85 | end 86 | -------------------------------------------------------------------------------- /bin/configliere-decrypt: -------------------------------------------------------------------------------- 1 | configliere -------------------------------------------------------------------------------- /bin/configliere-delete: -------------------------------------------------------------------------------- 1 | configliere -------------------------------------------------------------------------------- /bin/configliere-dump: -------------------------------------------------------------------------------- 1 | configliere -------------------------------------------------------------------------------- /bin/configliere-encrypt: -------------------------------------------------------------------------------- 1 | configliere -------------------------------------------------------------------------------- /bin/configliere-list: -------------------------------------------------------------------------------- 1 | configliere -------------------------------------------------------------------------------- /bin/configliere-set: -------------------------------------------------------------------------------- 1 | configliere -------------------------------------------------------------------------------- /configliere.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "configliere" 8 | s.version = "0.4.18" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Infochimps"] 12 | s.date = "2012-12-05" 13 | s.description = "You've got a script. It's got some settings. Some settings are for this module, some are for that module. Most of them don't change. Except on your laptop, where the paths are different. Or when you're in production mode. Or when you're testing from the command line.\n\n \"\" So, Consigliere of mine, I think you should tell your Don what everyone knows. \"\" -- Don Corleone\n\nConfigliere manage settings from many sources: static constants, simple config files, environment variables, commandline options, straight ruby. You don't have to predefine anything, but you can ask configliere to type-convert, require, document or password-obscure any of its fields. Modules can define config settings independently of each other and the main program.\n" 14 | s.email = "coders@infochimps.org" 15 | s.extra_rdoc_files = [ 16 | "LICENSE.md", 17 | "README.textile" 18 | ] 19 | s.files = [ 20 | ".rspec", 21 | ".travis.yml", 22 | ".yardopts", 23 | "CHANGELOG.textile", 24 | "FEATURES.txt", 25 | "Gemfile", 26 | "Guardfile", 27 | "LICENSE.md", 28 | "README.textile", 29 | "Rakefile", 30 | "VERSION", 31 | "bin/configliere", 32 | "bin/configliere-decrypt", 33 | "bin/configliere-delete", 34 | "bin/configliere-dump", 35 | "bin/configliere-encrypt", 36 | "bin/configliere-list", 37 | "bin/configliere-set", 38 | "configliere.gemspec", 39 | "examples/config_block_script.rb", 40 | "examples/encrypted_script.rb", 41 | "examples/env_var_script.rb", 42 | "examples/help_message_demo.rb", 43 | "examples/independent_config.rb", 44 | "examples/joke.rb", 45 | "examples/prompt.rb", 46 | "examples/simple_script.rb", 47 | "examples/simple_script.yaml", 48 | "lib/configliere.rb", 49 | "lib/configliere/commandline.rb", 50 | "lib/configliere/commands.rb", 51 | "lib/configliere/config_block.rb", 52 | "lib/configliere/config_file.rb", 53 | "lib/configliere/crypter.rb", 54 | "lib/configliere/deep_hash.rb", 55 | "lib/configliere/define.rb", 56 | "lib/configliere/encrypted.rb", 57 | "lib/configliere/env_var.rb", 58 | "lib/configliere/param.rb", 59 | "lib/configliere/prompt.rb", 60 | "lib/configliere/vayacondios.rb", 61 | "spec/configliere/commandline_spec.rb", 62 | "spec/configliere/commands_spec.rb", 63 | "spec/configliere/config_block_spec.rb", 64 | "spec/configliere/config_file_spec.rb", 65 | "spec/configliere/deep_hash_spec.rb", 66 | "spec/configliere/define_spec.rb", 67 | "spec/configliere/encrypted_spec.rb", 68 | "spec/configliere/env_var_spec.rb", 69 | "spec/configliere/param_spec.rb", 70 | "spec/configliere/prompt_spec.rb", 71 | "spec/configliere_spec.rb", 72 | "spec/spec.opts", 73 | "spec/spec_helper.rb" 74 | ] 75 | s.homepage = "https://github.com/infochimps-labs/configliere" 76 | s.licenses = ["Apache 2.0"] 77 | s.require_paths = ["lib"] 78 | s.rubygems_version = "1.8.15" 79 | s.summary = "Wise, discreet configuration management" 80 | 81 | if s.respond_to? :specification_version then 82 | s.specification_version = 3 83 | 84 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 85 | s.add_runtime_dependency(%q, ["~> 1.10.1"]) 86 | s.add_runtime_dependency(%q, ["1.5.2"]) 87 | s.add_dependency(%q, ["10.1.1"]) 88 | s.add_dependency(%q, ["0.8.7.3"]) 89 | s.add_dependency(%q, ["~> 2.14"]) 90 | s.add_dependency(%q, ["1.8.4"]) 91 | else 92 | s.add_dependency(%q, ["~> 1.10.1"]) 93 | s.add_dependency(%q, [">= 1.5.2"]) 94 | s.add_dependency(%q, ["~> 1.1"]) 95 | s.add_dependency(%q, [">= 0"]) 96 | s.add_dependency(%q, [">= 0.7"]) 97 | s.add_dependency(%q, ["~> 2.14"]) 98 | s.add_dependency(%q, [">= 1.6"]) 99 | end 100 | else 101 | s.add_dependency(%q, ["~> 1.10.1"]) 102 | s.add_dependency(%q, [">= 1.5.2"]) 103 | s.add_dependency(%q, ["~> 1.1"]) 104 | s.add_dependency(%q, [">= 0"]) 105 | s.add_dependency(%q, [">= 0.7"]) 106 | s.add_dependency(%q, ["~> 2.14"]) 107 | s.add_dependency(%q, [">= 1.6"]) 108 | end 109 | end 110 | 111 | -------------------------------------------------------------------------------- /examples/config_block_script.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.dirname(__FILE__)+'/../lib' 3 | require 'configliere' 4 | Settings.use :config_block 5 | 6 | Settings.define :passenger 7 | Settings.define :dest_time, :type => DateTime 8 | Settings :passenger => 'einstein', :dest_time => '1955-11-05' 9 | 10 | Settings.finally do |c| 11 | p ['(2) takes the settings object as arg', self, c[:passenger], c.passenger] 12 | # Einstein the dog should only be sent one minute into the future. 13 | c.dest_time = (Time.now + 60) if c.passenger == 'einstein' 14 | end 15 | 16 | Settings.finally{ p ['(3) note that blocks go in order'] } 17 | 18 | Settings.define :mc_fly, :default => 'wuss', 19 | :finally => lambda{ p ['(4) here is a block in the define'] ; Settings.mc_fly = 'badass' } 20 | 21 | p ["(1) :finally blocks are called when you invoke resolve!"] 22 | Settings.resolve! 23 | p ['(5) here are the settings', Settings] 24 | -------------------------------------------------------------------------------- /examples/encrypted_script.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.dirname(__FILE__)+'/../lib' 3 | require 'configliere' 4 | DUMP_FILENAME = '/tmp/encrypted_script.yml' 5 | 6 | # 7 | # Example usage: 8 | # export ENCRYPT_PASS=password1 9 | # ./examples/encrypted_script.rb 10 | # 11 | 12 | def dump_settings 13 | puts " #{Settings.inspect} -- #{Settings.encrypt_pass}" 14 | end 15 | 16 | puts %Q{Many times, scripts need to save values you\'d rather not leave as 17 | plaintext: API keys, database passwords, etc. Instead of leaving them in plain 18 | sight, you may wish to obscure their value on disk and use a secondary password 19 | to unlock it. 20 | 21 | View source for this script to see commands you might use (from the irb console 22 | or in a standalone script) to store the obscured values for later decryption.} 23 | 24 | Settings.use :config_file 25 | Settings.encrypt_pass = 'password1' 26 | Settings.define :secret, :encrypted => true, :default => 'plaintext' 27 | Settings.resolve! 28 | 29 | puts "\nIn-memory version still has secret in plaintext..." 30 | dump_settings 31 | puts "But the saved version will have encrypted secret (see #{DUMP_FILENAME}):" 32 | puts " #{Settings.send(:export).inspect}" 33 | Settings.save!(DUMP_FILENAME) 34 | 35 | puts "Let's reset the Settings:" 36 | Settings.delete :secret 37 | dump_settings 38 | puts "If we now load the saved file, the parameter's value is decrypted on resolve:" 39 | Settings.read('/tmp/encrypted_script.yml') 40 | begin 41 | Settings.resolve! 42 | rescue StandardError => e 43 | warn " #{e.class}: #{e}" 44 | warn "\nTry rerunning with \n ENCRYPT_PASS=password1 #{$0} #{$ARGV}" 45 | end 46 | dump_settings 47 | 48 | puts %Q{\nOf course, in your script you\'ll have to supply the decryption 49 | password. The best thing is to use an environment variable -- a user can spy on 50 | your commandline parameters using "ps" or "top". The following will fail unless 51 | you supply the correct password ("password1") in the ENCRYPT_PASS environment 52 | variable:\n\n} 53 | 54 | Settings.encrypt_pass = ENV['ENCRYPT_PASS'] # this will happen normally, but we overrode it above 55 | 56 | Settings.read('/tmp/encrypted_script.yml') 57 | begin 58 | Settings.resolve! 59 | puts "You guessed the password!" 60 | dump_settings 61 | rescue StandardError => e 62 | warn " #{e.class}: #{e}" 63 | warn "\nTry rerunning with \n ENCRYPT_PASS=password1 #{$0} #{$ARGV}" 64 | end 65 | -------------------------------------------------------------------------------- /examples/env_var_script.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.dirname(__FILE__)+'/../lib' 3 | require 'configliere' 4 | 5 | Settings.use :env_var, :commandline 6 | Settings.define :underpants, :env_var => 'UNDERPANTS', :default => "boxers" 7 | Settings.resolve! 8 | 9 | puts %Q{ 10 | Configliere can take parameter values from its defaults, from the commandline, or from the environment. 11 | Compare: 12 | 13 | ./env_var_script.rb # value from default 14 | ./env_var_script.rb --underpants=briefs # value from commandline 15 | UNDERPANTS="commando" ./env_var_script.rb # value from environment variable 16 | UNDERPANTS="commando" ./env_var_script.rb --underpants=briefs # commandline wins 17 | 18 | } 19 | 20 | puts %Q{Using 21 | * the default setting of: #{Settings.definition_of(:underpants, :default).inspect} 22 | * the environment variable: #{ENV['UNDERPANTS'].inspect} 23 | * the commandline setting: #{ARGV.grep(/^--underpants/).inspect} 24 | your configliere advises that 25 | #{Settings.inspect} 26 | } 27 | -------------------------------------------------------------------------------- /examples/help_message_demo.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.dirname(__FILE__)+"/../lib" 3 | require 'configliere' 4 | Settings.use :commandline 5 | 6 | Settings.define :logfile, :type => String, :description => "Log file name", :default => 'myapp.log', :required => false 7 | Settings.define :debug, :type => :boolean, :description => "Log debug messages to console?", :required => false 8 | Settings.define :dest_time, :type => DateTime, :description => "Arrival time", :required => true 9 | Settings.define :takes_opt, :flag => 't', :description => "Takes a single-letter flag '-t'" 10 | Settings.define :foobaz, :internal => true, :description => "You won't see me" 11 | Settings.define 'delorean.power_source', :env_var => 'POWER_SOURCE', :description => 'Delorean subsytem supplying power to the Flux Capacitor.' 12 | Settings.define :password, :required => true, :encrypted => true 13 | Settings.description = 'This is a sample script to demonstrate the help message. Notice how pretty everything lines up YAY' 14 | 15 | Settings.resolve! rescue nil 16 | puts "Run me again with --help to see the auto-generated help message!" 17 | -------------------------------------------------------------------------------- /examples/independent_config.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.dirname(__FILE__)+'/../lib' 3 | require 'configliere' 4 | 5 | class Wolfman 6 | attr_accessor :config 7 | def config 8 | @config || = Configliere::Param.new.use(:commandline).defaults({ 9 | :moon => 'full', 10 | :nards => true, 11 | }) 12 | end 13 | end 14 | 15 | teen_wolf = Wolfman.new 16 | 17 | teen_wolf.config.description = 'Run this with commandline args: Wolfman uses them, Settings does not' 18 | teen_wolf.config.defaults(:give_me => 'keg of beer') 19 | 20 | teen_wolf.config.resolve! 21 | Settings.resolve! 22 | 23 | # run this with ./examples/independent_config.rb --hi=there : 24 | puts "If you run this with #{$0} --hi=there, you should expect:" 25 | puts '{:moon=>"full", :nards=>true, :give_me=>"keg of beer", :hi=>"there"}' 26 | p teen_wolf.config 27 | 28 | puts 'the Settings hash should be empty:' 29 | p Settings #=> {} 30 | -------------------------------------------------------------------------------- /examples/joke.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.dirname(__FILE__)+'/../lib' 3 | require 'configliere' 4 | 5 | Settings.use :commands 6 | 7 | Settings.define_command :q1, :description => "The first joke (question)" 8 | Settings.define_command :q2, :description => "The second joke (question)" 9 | Settings.define_command :q3, :description => "The third joke (question)" 10 | 11 | Settings.define_command :a1, :description => "The first joke (answer -- all ages)" 12 | Settings.define_command :a2, :description => "The second joke (answer -- PG-13, themes of death)" do |cmd| 13 | cmd.define :age_limit, :type => Integer, :default => 13, :description => "minimum age to be able to enjoy joke 2" 14 | end 15 | Settings.define_command :a3, :description => "The third joke (answer -- R, strong language)" do |cmd| 16 | cmd.define :age_limit, :type => Integer, :default => 17, :description => "minimum age to be able to enjoy joke 3" 17 | cmd.define :bleep, :type => :boolean, :default => false, :description => "if enabled, solecisms will be bowdlerized" 18 | end 19 | 20 | Settings.define :debug, :type => :boolean, :default => false, :description => "show verbose progress", :internal => true 21 | Settings.define :age, :type => Integer, :required => true, :description => "Your age, in human years" 22 | Settings.define :fake_id, :type => :boolean, :default => false, :description => "A fake ID might be enough to bypass the age test" 23 | 24 | class Clown 25 | 26 | def q1 27 | puts "what do you call a deer with no eyes?" 28 | end 29 | 30 | def a1 31 | q1 32 | puts "no-eye deer" 33 | end 34 | 35 | def q2 36 | puts "what do you call a dead deer with no eyes?" 37 | end 38 | 39 | def a2 40 | q2 41 | puts "still no-eye deer" 42 | end 43 | 44 | def q3 45 | puts "what do you call a dead, castrated deer with no eyes?" 46 | end 47 | 48 | def a3 49 | q3 50 | puts "still no-#{Settings[:bleep] ? 'bleeping' : 'fucking'} no-eye deer" 51 | end 52 | 53 | def check_age_limit! 54 | return if not Settings[:age_limit] 55 | if Settings[:fake_id] 56 | puts "ok but you didn't hear it from me...\n\n" 57 | return true 58 | end 59 | if (Settings.age < Settings[:age_limit]) 60 | warn "Sorry kid, you're too young for this joke. Try this again when you're older (or maybe ask for --help)" 61 | exit(1) 62 | end 63 | end 64 | 65 | def check_command! 66 | if not Settings.command_name 67 | Settings.die "Which joke would you like to hear?" 68 | end 69 | end 70 | 71 | def run 72 | check_command! 73 | check_age_limit! 74 | self.public_send(Settings.command_name) 75 | end 76 | end 77 | 78 | Settings.resolve! 79 | 80 | if Settings.debug 81 | puts " -- received command #{Settings.command_name}, settings #{Settings}" 82 | puts "" 83 | puts "" 84 | end 85 | 86 | 87 | Clown.new.run 88 | -------------------------------------------------------------------------------- /examples/prompt.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.dirname(__FILE__)+'/../lib' 3 | require 'configliere' 4 | 5 | Settings.use :commandline, :prompt 6 | Settings.define :underpants, :description => 'boxers or briefs' 7 | Settings.resolve! 8 | 9 | puts %Q{ 10 | Configliere can prompt for a parameter value if none is given. 11 | If you call this with a value for --underpants, you will not see a prompt 12 | } 13 | 14 | puts %Q{Using the commandline setting #{ARGV.grep(/^--underpants/).inspect} 15 | your configliere advises that the settings are 16 | #{Settings.inspect} 17 | } 18 | 19 | puts Settings.prompt_for(:underpants) 20 | 21 | puts %Q{Now the Settings are 22 | #{Settings.inspect} 23 | } 24 | -------------------------------------------------------------------------------- /examples/simple_script.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.dirname(__FILE__)+'/../lib' 3 | require 'configliere' 4 | 5 | puts "This is a demo of Configliere in a simple script." 6 | Settings.use :commandline 7 | 8 | puts "\n\nSet default values inline:" 9 | 10 | Settings({ 11 | :heavy => true, 12 | :delorean => { 13 | :power_source => 'plutonium', 14 | :roads_needed => true, 15 | }, 16 | :username => 'marty', 17 | }) 18 | puts "\n #{Settings.inspect}" 19 | 20 | puts "\nYou can define settings' type, default value, and description (that shows up with --help), and more. It's purely optional, but it's very convenient:" 21 | 22 | Settings.define :dest_time, :default => '11-05-1955', :type => Time, :description => "Target date" 23 | # This defines a 'deep key': it controls Settings[:delorean][:roads_needed] 24 | Settings.define 'delorean.roads_needed', :type => :boolean 25 | Settings.define 'username', :env_var => 'DRIVER' 26 | puts "\n #{Settings.inspect}" 27 | 28 | config_filename = File.dirname(__FILE__)+'/simple_script.yaml' 29 | puts "\nValues loaded from the file #{config_filename} merge with the existing defaults:" 30 | 31 | Settings.read config_filename 32 | puts "\n #{Settings.inspect}" 33 | 34 | puts %Q{\nFinally, call resolve! to load the commandline you gave (#{ARGV.inspect}), do type conversion (watch what happens to :dest_time), etc:} 35 | Settings.resolve! 36 | puts "\n #{Settings.inspect}" 37 | 38 | saved_filename = '/tmp/simple_script_saved.yaml' 39 | puts %Q{\nYou can save the defaults out to a config file -- go look in #{saved_filename}} 40 | Settings.save!(saved_filename) 41 | 42 | if ARGV.empty? 43 | puts %Q{\nTry running the script again, but supply some commandline args:\n 44 | DRIVER=doc #{$0} --dest_time=11-05-2015 --delorean.roads_needed=false --delorean.power_source="Mr. Fusion"} 45 | end 46 | puts 47 | 48 | -------------------------------------------------------------------------------- /examples/simple_script.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Settings for return 3 | :dest_time: 11-05-1985 4 | :delorean: 5 | :power_source: 1.21 jiggawatts 6 | -------------------------------------------------------------------------------- /lib/configliere.rb: -------------------------------------------------------------------------------- 1 | require 'date' # type conversion 2 | require 'time' # type conversion 3 | require 'fileutils' # so save! can mkdir 4 | require 'configliere/deep_hash' # magic hash for params 5 | require 'configliere/param' # params container 6 | require 'configliere/define' # define param behavior 7 | require 'configliere/config_file' # read / save! files 8 | 9 | # use(:encrypted) will bring in 'digest/sha2' and 'openssl' 10 | # use(:prompt) will bring in 'highline', which you must gem install 11 | # running the specs requires rspec and spork 12 | 13 | module Configliere 14 | RUBY_ENGINE = 'ruby' if not defined?(::RUBY_ENGINE) 15 | 16 | ALL_MIXINS = [:define, :config_file, :commandline, :encrypted, :env_var, :config_block, :commands, :prompt] 17 | def self.use *mixins 18 | mixins = ALL_MIXINS if mixins.include?(:all) || mixins.empty? 19 | mixins.each do |mixin| 20 | require "configliere/#{mixin}" 21 | end 22 | end 23 | 24 | end 25 | 26 | # Base class for Configliere errors. 27 | class Configliere::Error < StandardError ; end 28 | # Feature is deprecated, has or will leave the building 29 | class Configliere::DeprecatedError < Configliere::Error ; end 30 | 31 | # Defines a global config object 32 | Settings = Configliere::Param.new unless defined?(Settings) 33 | 34 | # 35 | # Also define Settings as a function, so you can say 36 | # Settings :this => that, :cat => :hat 37 | # 38 | def Settings *args 39 | Settings.defaults(*args) 40 | end 41 | -------------------------------------------------------------------------------- /lib/configliere/commandline.rb: -------------------------------------------------------------------------------- 1 | module Configliere 2 | 3 | # 4 | # Command line tool to manage param info 5 | # 6 | # @example 7 | # Settings.use :commandline 8 | # 9 | module Commandline 10 | attr_accessor :rest 11 | attr_accessor :description 12 | attr_reader :unknown_argvs 13 | 14 | # Processing to reconcile all options 15 | # 16 | # * processes all commandline params 17 | # * calls up the resolve! chain. 18 | # 19 | # * if the --help param was given, prints out a usage statement describing all #define'd params and exits 20 | # 21 | # If the --help param was given, Will print out a usage statement 22 | # describing all #define'd params and exit. To avoid this, pass 23 | # `false` as the first argument. 24 | # 25 | # @param [true,false] print_help_and_exit whether to print help and exit if the --help param was given 26 | def resolve!(print_help_and_exit=true) 27 | process_argv! 28 | if print_help_and_exit && self[:help] 29 | dump_help 30 | exit(2) 31 | end 32 | super() 33 | self 34 | end 35 | 36 | # 37 | # Parse the command-line args into the params hash. 38 | # 39 | # '--happy_flag' produces :happy_flag => true in the params hash 40 | # '--foo=foo_val' produces :foo => 'foo_val' in the params hash. 41 | # '--' Stop parsing; all remaining args are piled into :rest 42 | # 43 | # self.rest contains all arguments that don't start with a '--' 44 | # and all args following the '--' sentinel if any. 45 | # 46 | def process_argv! 47 | args = ARGV.dup 48 | self.rest = [] 49 | @unknown_argvs = [] 50 | until args.empty? do 51 | arg = args.shift 52 | case 53 | # end of options parsing 54 | when arg == '--' 55 | self.rest += args 56 | break 57 | # --param=val or --param 58 | when arg =~ /\A--([\w\-\.]+)(?:=(.*))?\z/ 59 | param, val = [$1, $2] 60 | warn "Configliere uses _underscores not dashes for params" if param.include?('-') 61 | @unknown_argvs << param.to_sym if (not has_definition?(param)) 62 | self[param] = parse_value(val) 63 | # -abc 64 | when arg =~ /\A-(\w\w+)\z/ 65 | $1.each_char do |flag| 66 | param = find_param_for_flag(flag) 67 | unless param then @unknown_argvs << flag ; next ; end 68 | self[param] = true 69 | end 70 | # -a val 71 | when arg =~ /\A-(\w)\z/ 72 | flag = find_param_for_flag($1) 73 | unless flag then @unknown_argvs << flag ; next ; end 74 | if (not args.empty?) && (args.first !~ /\A-/) 75 | val = args.shift 76 | else 77 | val = nil 78 | end 79 | self[flag] = parse_value(val) 80 | # -a=val 81 | when arg =~ /\A-(\w)=(.*)\z/ 82 | flag, val = [find_param_for_flag($1), $2] 83 | unless flag then @unknown_argvs << flag ; next ; end 84 | self[flag] = parse_value(val) 85 | else 86 | self.rest << arg 87 | end 88 | end 89 | @unknown_argvs.uniq! 90 | end 91 | 92 | # =========================================================================== 93 | # 94 | # Recyle out our settings as a commandline 95 | # 96 | 97 | # Returns a flag in dashed form, suitable for recycling into the commandline 98 | # of an external program. 99 | # Can specify a specific flag name, otherwise the given setting key is used 100 | # 101 | # @example 102 | # Settings.dashed_flag_for(:flux_capacitance) 103 | # #=> --flux-capacitance=0.7 104 | # Settings.dashed_flag_for(:screw_you, :hello_friend) 105 | # #=> --hello-friend=true 106 | # 107 | def dashed_flag_for setting_name, flag_name=nil 108 | return unless self[setting_name] 109 | flag_name ||= setting_name 110 | (self[setting_name] == true ? "--#{flag_name.to_s.gsub(/_/,"-")}" : "--#{flag_name.to_s.gsub(/_/,"-")}=#{self[setting_name]}" ) 111 | end 112 | 113 | # dashed_flag_for each given setting that has a value 114 | def dashed_flags *settings_and_names 115 | settings_and_names.map{|args| dashed_flag_for(*args) }.compact 116 | end 117 | 118 | # =========================================================================== 119 | # 120 | # Commandline help 121 | # 122 | 123 | # Write the help string to stderr 124 | def dump_help str=nil 125 | warn help(str)+"\n" 126 | end 127 | 128 | # The contents of the help message. 129 | # Lists the usage as well as any defined parameters and environment variables 130 | def help str=nil 131 | buf = [] 132 | buf << usage 133 | buf << "\n"+@description if @description 134 | buf << param_lines 135 | buf << commands_help if respond_to?(:commands_help) 136 | buf << "\n\n"+str if str 137 | buf.flatten.compact.join("\n")+"\n" 138 | end 139 | 140 | # Usage line 141 | def usage 142 | %Q{usage: #{raw_script_name} [...--param=val...]} 143 | end 144 | attr_writer :usage 145 | 146 | # the script basename, for recycling into help messages 147 | def raw_script_name 148 | File.basename($0) 149 | end 150 | 151 | # die with a warning 152 | # 153 | # @example 154 | # Settings.define :foo, :finally => lambda{ Settings.foo.to_i < 5 or die("too much foo!") } 155 | # 156 | # @param str [String] the string to dump out before exiting 157 | # @param exit_code [Integer] UNIX exit code to set, default -1 158 | def die str, exit_code=-1 159 | dump_help "****\n#{str}\n****" 160 | exit exit_code 161 | end 162 | 163 | protected 164 | 165 | # handle --param (true), --param='' (set as ++nil++), --param=hi ('hi') 166 | def parse_value val 167 | case 168 | when val == nil then true # --flag option on its own means 'set that option' 169 | when val == '' then nil # --flag='' the explicit empty string means nil 170 | else val # else just return the value 171 | end 172 | end 173 | 174 | # Retrieve the first param defined with the given flag. 175 | def find_param_for_flag(flag) 176 | params_with(:flag).each do |param_name, param_flag| 177 | return param_name if flag.to_s == param_flag.to_s 178 | end 179 | nil 180 | end 181 | 182 | def param_lines 183 | pdefs = definitions.reject{|name, definition| definition[:internal] } 184 | return if pdefs.empty? 185 | buf = ["\nParams:"] 186 | width = find_width(pdefs.keys) 187 | has_flags = (not params_with(:flag).empty?) 188 | pdefs.sort_by{|pn, pd| pn.to_s }.each do |name, definition| 189 | buf << param_line(name, definition, width, has_flags) 190 | end 191 | buf 192 | end 193 | 194 | # pretty-print a param 195 | def param_line(name, definition, width, has_flags) 196 | desc = definition_of(name, :description).to_s.strip 197 | buf = [' '] 198 | buf << (definition[:flag] ? "-#{definition[:flag]}," : " ") if has_flags 199 | buf << sprintf("--%-#{width}s", param_with_type(name)) 200 | buf << (desc.empty? ? name : desc) 201 | buf << "[Default: #{definition[:default]}]" if definition[:default] 202 | buf << '[Required]' if definition[:required] 203 | buf << '[Encrypted]' if definition[:encrypted] 204 | buf << "[Env Var: #{definition[:env_var]}]" if definition[:env_var] 205 | buf.join(' ') 206 | end 207 | 208 | # run through the params and find the width needed to pretty-print them 209 | def find_width(param_names) 210 | [ 20, 211 | param_names.map{|param_name| param_with_type(param_name).length } 212 | ].flatten.max + 2 213 | end 214 | 215 | def param_with_type(param) 216 | str = param.to_s 217 | type = definition_of(param, :type) 218 | case type 219 | when :boolean then str += '' 220 | when nil then str += '=String' 221 | else str += "=#{type}" 222 | end 223 | str 224 | end 225 | 226 | end 227 | 228 | Param.on_use(:commandline) do 229 | extend Configliere::Commandline 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /lib/configliere/commands.rb: -------------------------------------------------------------------------------- 1 | module Configliere 2 | 3 | # 4 | # Command line tool to manage param info 5 | # 6 | # To include, specify 7 | # 8 | # Settings.use :commands 9 | # 10 | module Commands 11 | # The name of the command. 12 | attr_accessor :command_name 13 | 14 | # 15 | # FIXME: this will be refactored to look like Configliere::Define 16 | # 17 | 18 | # Add a command, along with a description of its predicates and the command itself. 19 | def define_command cmd, options={}, &block 20 | cmd = cmd.to_sym 21 | command_configuration = Configliere::Param.new 22 | command_configuration.use :commandline, :env_var 23 | yield command_configuration if block_given? 24 | commands[cmd] = options 25 | commands[cmd][:config] = command_configuration 26 | commands[cmd] 27 | end 28 | 29 | def commands 30 | @commands ||= DeepHash.new 31 | end 32 | 33 | def command_info 34 | commands[command_name] if command_name 35 | end 36 | 37 | def resolve! 38 | super() 39 | commands.each do |cmd, cmd_info| 40 | cmd_info[:config].resolve! 41 | end 42 | if command_name && commands[command_name] 43 | sub_config = commands[command_name][:config] 44 | adoptable = sub_config.send(:definitions).keys 45 | merge!( Hash[sub_config.select{|k,v| adoptable.include?(k) }] ) 46 | end 47 | self 48 | end 49 | 50 | # 51 | # Parse the command-line args into the params hash. 52 | # 53 | # '--happy_flag' produces :happy_flag => true in the params hash 54 | # '--foo=foo_val' produces :foo => 'foo_val' in the params hash. 55 | # '--' Stop parsing; all remaining args are piled into :rest 56 | # 57 | # self.rest contains all arguments that don't start with a '--' 58 | # and all args following the '--' sentinel if any. 59 | # 60 | def process_argv! 61 | super() 62 | base, cmd = script_base_and_command 63 | if cmd 64 | self.command_name = cmd.to_sym 65 | elsif (not rest.empty?) && commands.include?(rest.first.to_sym) 66 | self.command_name = rest.shift.to_sym 67 | end 68 | end 69 | 70 | # Usage line 71 | def usage 72 | %Q{usage: #{script_base_and_command.first} [command] [...--param=val...]} 73 | end 74 | 75 | protected 76 | 77 | # Return help on commands. 78 | def commands_help 79 | help = ["\nAvailable commands:"] 80 | commands.sort_by(&:to_s).each do |cmd, info| 81 | help << (" %-27s %s" % [cmd, info[:description]]) unless info[:internal] 82 | info[:config].param_lines[1..-1].each{|line| help << " #{line}" } rescue nil 83 | end 84 | help << "\nRun `#{script_base_and_command.first} help COMMAND' for more help on COMMAND" if commands.include?(:help) 85 | help.flatten.join("\n") 86 | end 87 | 88 | # The script name without command appendix if any: For $0 equal to any of 89 | # 'git', 'git-reset', or 'git-cherry-pick', base_script_name is 'git' 90 | # 91 | def script_base_and_command 92 | raw_script_name.split('-', 2) 93 | end 94 | end 95 | 96 | Param.on_use(:commands) do 97 | use :commandline 98 | extend Configliere::Commands 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/configliere/config_block.rb: -------------------------------------------------------------------------------- 1 | module Configliere 2 | # 3 | # ConfigBlock lets you use pure ruby to change and define settings. Call 4 | # +#finally+ with a block of code to be run after all other settings are in 5 | # place. 6 | # 7 | # Settings.finally{|c| c.your_mom[:college] = 'went' unless (! c.mom_jokes_allowed) } 8 | # 9 | module ConfigBlock 10 | # 11 | # Define a block of code to run after all other settings are in place. 12 | # 13 | # @param &block each +finally+ block is called once, in the order it was 14 | # defined, when the resolve! method is invoked. +config_block+ resolution 15 | # is guaranteed to run last in the resolve chain, right before validation. 16 | # 17 | # @example 18 | # Settings.finally do |c| 19 | # c.dest_time = (Time.now + 60) if c.username == 'einstein' 20 | # # you can use hash syntax too 21 | # c[:dest_time] = (Time.now + 60) if c[:username] == 'einstein' 22 | # end 23 | # # ... 24 | # # after rest of setup: 25 | # Settings.resolve! 26 | # 27 | def finally &block 28 | self.final_blocks << block if block 29 | end 30 | 31 | # Processing to reconcile all options 32 | # 33 | # The resolve! for config_block is made to run last of all in the +resolve!+ 34 | # chain, and runs each +finally+ block in the order it was defined. 35 | def resolve! 36 | super 37 | resolve_finally_blocks! 38 | self 39 | end 40 | 41 | protected 42 | # Config blocks to be executed at end of resolution (just before validation) 43 | attr_accessor :final_blocks 44 | def final_blocks 45 | @final_blocks ||= [] 46 | end 47 | 48 | # call each +finally+ config block in the order it was defined 49 | def resolve_finally_blocks! 50 | final_blocks.each do |block| 51 | (block.arity == 1) ? block.call(self) : block.call() 52 | end 53 | end 54 | end 55 | 56 | Param.class_eval do 57 | include Configliere::ConfigBlock 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/configliere/config_file.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module Configliere 4 | # Default locations where config files live 5 | DEFAULT_CONFIG_LOCATION = { 6 | :machine => lambda{|scope| Pathname('/etc').join(scope) }, 7 | :user => lambda{|scope| Pathname(ENV['HOME'] || '/').join(".#{scope}") }, 8 | :app => lambda{|scope| app_dir = Pathname('/') ; Pathname(Dir.pwd).ascend{ |path| app_dir = path.join('config') if path.join('config').exist? } ; app_dir } 9 | } unless defined?(DEFAULT_CONFIG_LOCATION) 10 | 11 | # 12 | # ConfigFile -- load configuration from a simple YAML file 13 | # 14 | module ConfigFile 15 | # Load params from a YAML file, as a hash of handle => param_hash pairs 16 | # 17 | # @param filename [String] the file to read. If it does not contain a '/', 18 | # the filename is expanded relative to Configliere::DEFAULT_CONFIG_DIR 19 | # @param options [Hash] 20 | # @option options :env [String] 21 | # If an :env option is given, only the indicated subhash is merged. This 22 | # lets you for example specify production / environment / test settings 23 | # 24 | # @return [Configliere::Params] the Settings object 25 | # 26 | # @example 27 | # # Read from ~/.configliere/foo.yaml 28 | # Settings.read(foo.yaml) 29 | # 30 | # @example 31 | # # Read from config/foo.yaml and use settings appropriate for development/staging/production 32 | # Settings.read(App.root.join('config', 'environment.yaml'), :env => ENV['RACK_ENV']) 33 | # 34 | # The env option is *not* coerced to_sym, so make sure your key type matches the file's 35 | def read filename, options={} 36 | if filename.is_a?(Symbol) then raise Configliere::DeprecatedError, "Loading from a default config file is no longer provided" ; end 37 | filename = expand_filename(filename) 38 | begin 39 | case filetype(filename) 40 | when 'json' then read_json(File.open(filename), options) 41 | when 'yaml' then read_yaml(File.open(filename), options) 42 | else read_yaml(File.open(filename), options) 43 | end 44 | rescue Errno::ENOENT 45 | warn "Loading empty configliere settings file #{filename}" 46 | end 47 | self 48 | end 49 | 50 | def read_yaml yaml_str, options={} 51 | require 'yaml' 52 | new_data = YAML.load(yaml_str) || {} 53 | # Extract the :env (production/development/etc) 54 | if options[:env] 55 | new_data = new_data[options[:env]] || {} 56 | end 57 | deep_merge! new_data 58 | self 59 | end 60 | 61 | # 62 | # we depend on you to require some sort of JSON 63 | # 64 | def read_json json_str, options={} 65 | require 'multi_json' 66 | new_data = MultiJson.load(json_str) || {} 67 | # Extract the :env (production/development/etc) 68 | if options[:env] 69 | new_data = new_data[options[:env]] || {} 70 | end 71 | deep_merge! new_data 72 | self 73 | end 74 | 75 | # save to disk. 76 | # * file is in YAML format, as a hash of handle => param_hash pairs 77 | # * filename defaults to Configliere::DEFAULT_CONFIG_FILE (~/.configliere, probably) 78 | def save! filename 79 | filename = expand_filename(filename) 80 | hsh = self.export.to_hash 81 | FileUtils.mkdir_p(File.dirname(filename)) 82 | File.open(filename, 'w'){|file| file << YAML.dump(hsh) } 83 | end 84 | 85 | def determine_conf_location(level, scope) 86 | lookup_conf_dir(level, scope).join("#{scope}.yaml").to_s 87 | end 88 | 89 | def default_conf_dir 90 | lookup_conf_dir(:user, 'configliere') 91 | end 92 | 93 | def lookup_conf_dir(level, scope) 94 | Configliere::DEFAULT_CONFIG_LOCATION[level].call(scope) 95 | end 96 | 97 | def load_configuration_in_order!(scope = 'configliere') 98 | [ :machine, :user, :app ].each do |level| 99 | conf = determine_conf_location(level, scope) 100 | read(conf) if Pathname(conf).exist? 101 | end 102 | resolve! 103 | end 104 | 105 | protected 106 | 107 | def filetype filename 108 | filename =~ /\.([^\.]+)$/ ; 109 | $1 110 | end 111 | 112 | def expand_filename filename 113 | if filename.to_s.include?('/') 114 | File.expand_path(filename) 115 | else 116 | default_conf_dir.join(filename).to_s 117 | end 118 | end 119 | end 120 | 121 | # ConfigFile is included by default 122 | Param.class_eval do 123 | include ConfigFile 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/configliere/crypter.rb: -------------------------------------------------------------------------------- 1 | module Configliere 2 | # for encryption 3 | 4 | begin 5 | require 'openssl' 6 | PLATFORM_ENCRYPTION_ERROR = nil 7 | rescue LoadError => err 8 | raise unless err.to_s.include?('openssl') 9 | warn "Your ruby doesn't appear to have been built with OpenSSL." 10 | warn "So you don't get to have Encryption." 11 | PLATFORM_ENCRYPTION_ERROR = err 12 | end 13 | 14 | require 'digest/sha2' 15 | # base64-encode the binary encrypted string 16 | require "base64" 17 | 18 | # 19 | # Encrypt and decrypt values in configliere stores 20 | # 21 | module Crypter 22 | CIPHER_TYPE = "aes-256-cbc" unless defined?(CIPHER_TYPE) 23 | 24 | def self.check_platform_can_encrypt! 25 | return true unless PLATFORM_ENCRYPTION_ERROR 26 | raise PLATFORM_ENCRYPTION_ERROR.class, "Encryption broken on this platform: #{PLATFORM_ENCRYPTION_ERROR}" 27 | end 28 | 29 | # 30 | # Encrypt the given string 31 | # 32 | # @param plaintext the text to encrypt 33 | # @param [String] encrypt_pass secret passphrase to encrypt with 34 | # @return [String] encrypted text, suitable for deciphering with Crypter#decrypt 35 | # 36 | def self.encrypt plaintext, encrypt_pass, options={} 37 | # The cipher's IV (Initialization Vector) is prepended (unencrypted) to 38 | # the ciphertext, which as far as I can tell is safe for our purposes: 39 | # http://www.ciphersbyritter.com/NEWS6/CBCIV.HTM 40 | cipher = new_cipher :encrypt, encrypt_pass, options 41 | cipher.iv = iv = cipher.random_iv 42 | ciphertext = cipher.update(plaintext) 43 | ciphertext << cipher.final 44 | Base64.encode64(combine_iv_and_ciphertext(iv, ciphertext)) 45 | end 46 | # 47 | # Decrypt the given string, using the key and iv supplied 48 | # 49 | # @param ciphertext the text to decrypt, probably produced with Crypter#decrypt 50 | # @param [String] encrypt_pass secret passphrase to decrypt with 51 | # @return [String] the decrypted plaintext 52 | # 53 | def self.decrypt enc_ciphertext, encrypt_pass, options={} 54 | iv_and_ciphertext = Base64.decode64(enc_ciphertext) 55 | cipher = new_cipher :decrypt, encrypt_pass, options 56 | cipher.iv, ciphertext = separate_iv_and_ciphertext(cipher, iv_and_ciphertext) 57 | plaintext = cipher.update(ciphertext) 58 | plaintext << cipher.final 59 | plaintext 60 | end 61 | protected 62 | # 63 | # Create a new cipher machine, with its dials set in the given direction 64 | # 65 | # @param [:encrypt, :decrypt] direction whether to encrypt or decrypt 66 | # @param [String] encrypt_pass secret passphrase to decrypt with 67 | # 68 | def self.new_cipher direction, encrypt_pass, options={} 69 | check_platform_can_encrypt! 70 | cipher = OpenSSL::Cipher::Cipher.new(CIPHER_TYPE) 71 | case direction when :encrypt then cipher.encrypt when :decrypt then cipher.decrypt else raise "Bad cipher direction #{direction}" end 72 | cipher.key = encrypt_key(encrypt_pass, options) 73 | cipher 74 | end 75 | 76 | # prepend the initialization vector to the encoded message 77 | def self.combine_iv_and_ciphertext iv, message 78 | message.force_encoding("BINARY") if message.respond_to?(:force_encoding) 79 | iv.force_encoding("BINARY") if iv.respond_to?(:force_encoding) 80 | iv + message 81 | end 82 | # pull the initialization vector from the front of the encoded message 83 | def self.separate_iv_and_ciphertext cipher, iv_and_ciphertext 84 | idx = cipher.iv_len 85 | [ iv_and_ciphertext[0..(idx-1)], iv_and_ciphertext[idx..-1] ] 86 | end 87 | 88 | # Convert the encrypt_pass passphrase into the key used for encryption 89 | def self.encrypt_key encrypt_pass, options={} 90 | encrypt_pass = encrypt_pass.to_s 91 | raise 'Missing encryption password!' if encrypt_pass.empty? 92 | # this provides the required 256 bits of key for the aes-256-cbc cipher 93 | Digest::SHA256.digest(encrypt_pass) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/configliere/deep_hash.rb: -------------------------------------------------------------------------------- 1 | # 2 | # A magical hash: allows deep-nested access and proper merging of settings from 3 | # different sources 4 | # 5 | class DeepHash < Hash 6 | 7 | # @param constructor 8 | # The default value for the DeepHash. Defaults to an empty hash. 9 | # If constructor is a Hash, adopt its values. 10 | def initialize(constructor = {}) 11 | if constructor.is_a?(Hash) 12 | super() 13 | update(constructor) unless constructor.empty? 14 | else 15 | super(constructor) 16 | end 17 | end 18 | 19 | unless method_defined?(:regular_writer) then alias_method :regular_writer, :[]= ; end 20 | unless method_defined?(:regular_update) then alias_method :regular_update, :update ; end 21 | 22 | # @param key The key to check for. This will be run through convert_key. 23 | # 24 | # @return [Boolean] True if the key exists in the mash. 25 | def key?(key) 26 | attr = convert_key(key) 27 | if attr.is_a?(Array) 28 | fk = attr.shift 29 | attr = attr.first if attr.length == 1 30 | super(fk) && (self[fk].key?(attr)) rescue nil 31 | else 32 | super(attr) 33 | end 34 | end 35 | 36 | # def include? def has_key? def member? 37 | alias_method :include?, :key? 38 | alias_method :has_key?, :key? 39 | alias_method :member?, :key? 40 | 41 | # @param key The key to fetch. This will be run through convert_key. 42 | # @param *extras Default value. 43 | # 44 | # @return [Object] The value at key or the default value. 45 | def fetch(key, *extras) 46 | super(convert_key(key), *extras) 47 | end 48 | 49 | # @param *indices 50 | # The keys to retrieve values for. These will be run through +convert_key+. 51 | # 52 | # @return [Array] The values at each of the provided keys 53 | def values_at(*indices) 54 | indices.collect{|key| self[convert_key(key)]} 55 | end 56 | 57 | # @param key 58 | # The key to delete from the DeepHash. 59 | def delete attr 60 | attr = convert_key(attr) 61 | attr.is_a?(Array) ? deep_delete(*attr) : super(attr) 62 | end 63 | 64 | # @return [Hash] converts to a plain hash. 65 | def to_hash 66 | Hash.new(default).merge(self) 67 | end 68 | 69 | # @param hash The hash to merge with the deep_hash. 70 | # 71 | # @return [DeepHash] A new deep_hash with the hash values merged in. 72 | def merge(hash, &block) 73 | self.dup.update(hash, &block) 74 | end 75 | 76 | # @param other_hash 77 | # A hash to update values in the deep_hash with. The keys and the values will be 78 | # converted to DeepHash format. 79 | # 80 | # @return [DeepHash] The updated deep_hash. 81 | def update(other_hash, &block) 82 | deep_hash = self.class.new 83 | other_hash.each_pair do |key, value| 84 | val = convert_value(value) 85 | deep_hash[key] = val 86 | end 87 | regular_update(deep_hash, &block) 88 | end 89 | 90 | alias_method :merge!, :update 91 | 92 | # Allows for reverse merging two hashes where the keys in the calling hash take precedence over those 93 | # in the other_hash. This is particularly useful for initializing an option hash with default values: 94 | # 95 | # def setup(options = {}) 96 | # options.reverse_merge! :size => 25, :velocity => 10 97 | # end 98 | # 99 | # Using merge, the above example would look as follows: 100 | # 101 | # def setup(options = {}) 102 | # { :size => 25, :velocity => 10 }.merge(options) 103 | # end 104 | # 105 | # The default :size and :velocity are only set if the +options+ hash passed in doesn't already 106 | # have the respective key. 107 | def reverse_merge(other_hash) 108 | self.class.new(other_hash).merge!(self) 109 | end unless method_defined?(:reverse_merge) 110 | 111 | # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second. 112 | # Modifies the receiver in place. 113 | def reverse_merge!(other_hash) 114 | merge!( other_hash ){|k,o,n| convert_value(o) } 115 | end unless method_defined?(:reverse_merge!) 116 | 117 | # This DeepHash with all its keys converted to symbols, as long as they 118 | # respond to +to_sym+. (this is always true for a deep_hash) 119 | # 120 | # @return [DeepHash] A copy of this deep_hash. 121 | def symbolize_keys 122 | dup.symbolize_keys! 123 | end unless method_defined?(:symbolize_keys) 124 | 125 | # This DeepHash with all its keys converted to symbols, as long as they 126 | # respond to +to_sym+. (this is always true for a deep_hash) 127 | # 128 | # @return [DeepHash] This deep_hash unchanged. 129 | def symbolize_keys!; self end 130 | 131 | # Return a new hash with all top-level keys converted to strings. 132 | # 133 | # @return [Hash] 134 | def stringify_keys 135 | hsh = Hash.new(default) 136 | self.each do |key, val| 137 | hsh[key.to_s] = val 138 | end 139 | hsh 140 | end 141 | 142 | # 143 | # remove all key-value pairs where the value is nil 144 | # 145 | def compact 146 | reject{|key,val| val.nil? } 147 | end 148 | # 149 | # Replace the hash with its compacted self 150 | # 151 | def compact! 152 | replace(compact) 153 | end 154 | 155 | # Slice a hash to include only the given keys. This is useful for 156 | # limiting an options hash to valid keys before passing to a method: 157 | # 158 | # def search(criteria = {}) 159 | # assert_valid_keys(:mass, :velocity, :time) 160 | # end 161 | # 162 | # search(options.slice(:mass, :velocity, :time)) 163 | # 164 | # If you have an array of keys you want to limit to, you should splat them: 165 | # 166 | # valid_keys = [:mass, :velocity, :time] 167 | # search(options.slice(*valid_keys)) 168 | def slice(*keys) 169 | keys = keys.map!{|key| convert_key(key) } if respond_to?(:convert_key) 170 | hash = self.class.new 171 | keys.each { |k| hash[k] = self[k] if has_key?(k) } 172 | hash 173 | end unless method_defined?(:slice) 174 | 175 | # Replaces the hash with only the given keys. 176 | # Returns a hash containing the removed key/value pairs 177 | # @example 178 | # hsh = {:a => 1, :b => 2, :c => 3, :d => 4} 179 | # hsh.slice!(:a, :b) 180 | # # => {:c => 3, :d =>4} 181 | # hsh 182 | # # => {:a => 1, :b => 2} 183 | def slice!(*keys) 184 | keys = keys.map!{|key| convert_key(key) } if respond_to?(:convert_key) 185 | omit = slice(*self.keys - keys) 186 | hash = slice(*keys) 187 | replace(hash) 188 | omit 189 | end unless method_defined?(:slice!) 190 | 191 | # Removes the given keys from the hash 192 | # Returns a hash containing the removed key/value pairs 193 | # 194 | # @example 195 | # hsh = {:a => 1, :b => 2, :c => 3, :d => 4} 196 | # hsh.extract!(:a, :b) 197 | # # => {:a => 1, :b => 2} 198 | # hsh 199 | # # => {:c => 3, :d =>4} 200 | def extract!(*keys) 201 | result = self.class.new 202 | keys.each{|key| result[key] = delete(key) } 203 | result 204 | end unless method_defined?(:extract!) 205 | 206 | # Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch. 207 | # Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols 208 | # as keys, this will fail. 209 | # 210 | # ==== Examples 211 | # { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years" 212 | # { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age" 213 | # { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing 214 | def assert_valid_keys(*valid_keys) 215 | unknown_keys = keys - [valid_keys].flatten 216 | raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty? 217 | end unless method_defined?(:assert_valid_keys) 218 | 219 | # Sets a member value. 220 | # 221 | # Given a deep key (one that contains '.'), uses it as a chain of hash 222 | # memberships. Otherwise calls the normal hash member setter 223 | # 224 | # @example 225 | # foo = DeepHash.new :hi => 'there' 226 | # foo['howdy.doody'] = 3 227 | # foo # => { :hi => 'there', :howdy => { :doody => 3 } } 228 | # 229 | def []= attr, val 230 | attr = convert_key(attr) 231 | val = convert_value(val) 232 | attr.is_a?(Array) ? deep_set(*(attr | [val])) : super(attr, val) 233 | end 234 | 235 | 236 | # Gets a member value. 237 | # 238 | # Given a deep key (one that contains '.'), uses it as a chain of hash 239 | # memberships. Otherwise calls the normal hash member getter 240 | # 241 | # @example 242 | # foo = DeepHash.new({ :hi => 'there', :howdy => { :doody => 3 } }) 243 | # foo['howdy.doody'] # => 3 244 | # foo['hi'] # => 'there' 245 | # foo[:hi] # => 'there' 246 | # 247 | def [] attr 248 | attr = convert_key(attr) 249 | attr.is_a?(Array) ? deep_get(*attr) : super(attr) 250 | end 251 | 252 | # lambda for recursive merges 253 | ::DeepHash::DEEP_MERGER = proc do |key,v1,v2| 254 | if (v1.respond_to?(:update) && v2.respond_to?(:update)) 255 | v1.update(v2.reject{|kk,vv| vv.nil? }, &DeepHash::DEEP_MERGER) 256 | elsif v2.nil? 257 | v1 258 | else 259 | v2 260 | end 261 | end unless defined?(::DeepHash::DEEP_MERGER) 262 | 263 | # 264 | # Merge hashes recursively. 265 | # Nothing special happens to array values 266 | # 267 | # x = { :subhash => { :a => :val_from_x, :b => :only_in_x, :c => :only_in_x }, :scalar => :scalar_from_x} 268 | # y = { :subhash => { :a => :val_from_y, :d => :only_in_y }, :scalar => :scalar_from_y } 269 | # x.deep_merge y 270 | # => {:subhash=>{:a=>:val_from_y, :b=>:only_in_x, :c=>:only_in_x, :d=>:only_in_y}, :scalar=>:scalar_from_y} 271 | # y.deep_merge x 272 | # => {:subhash=>{:a=>:val_from_x, :b=>:only_in_x, :c=>:only_in_x, :d=>:only_in_y}, :scalar=>:scalar_from_x} 273 | # 274 | # Nil values always lose. 275 | # 276 | # x = {:subhash=>{:nil_in_x=>nil, a=>:val_a}, :nil_in_x=>nil} 277 | # y = {:subhash=>{:nil_in_x=>5}, :nil_in_x=>5} 278 | # y.deep_merge x 279 | # => {:subhash=>{:a=>:val_a, :nil_in_x=>5}, :nil_in_x=>5} 280 | # x.deep_merge y 281 | # => {:subhash=>{:a=>:val_a, :nil_in_x=>5}, :nil_in_x=>5} 282 | # 283 | def deep_merge hsh2 284 | merge hsh2, &DeepHash::DEEP_MERGER 285 | end 286 | 287 | def deep_merge! hsh2 288 | update hsh2, &DeepHash::DEEP_MERGER 289 | self 290 | end 291 | 292 | # 293 | # Treat hash as tree of hashes: 294 | # 295 | # x = { :a => :val, :subhash => { :b => :val_b } } 296 | # x.deep_set(:subhash, :cat, :hat) 297 | # # => { :a => :val, :subhash => { :b => :val_b, :cat => :hat } } 298 | # x.deep_set(:subhash, :b, :newval) 299 | # # => { :a => :val, :subhash => { :b => :newval, :cat => :hat } } 300 | # 301 | # 302 | def deep_set *args 303 | val = args.pop 304 | last_key = args.pop 305 | # dig down to last subtree (building out if necessary) 306 | hsh = self 307 | args.each do |key| 308 | hsh.regular_writer(key, self.class.new) unless hsh.has_key?(key) 309 | hsh = hsh[key] 310 | end 311 | # set leaf value 312 | hsh[last_key] = val 313 | end 314 | 315 | # 316 | # Treat hash as tree of hashes: 317 | # 318 | # x = { :a => :val_a, :subhash => { :b => :val_b } } 319 | # x.deep_get(:a) 320 | # # => :val_a 321 | # x.deep_get(:subhash, :c) 322 | # # => nil 323 | # x.deep_get(:subhash, :c, :f) 324 | # # => nil 325 | # x.deep_get(:subhash, :b) 326 | # # => nil 327 | # 328 | def deep_get *args 329 | last_key = args.pop 330 | # dig down to last subtree (building out if necessary) 331 | hsh = args.inject(self){|h, k| h[k] || self.class.new } 332 | # get leaf value 333 | hsh[last_key] 334 | end 335 | 336 | # 337 | # Treat hash as tree of hashes: 338 | # 339 | # x = { :a => :val, :subhash => { :a => :val1, :b => :val2 } } 340 | # x.deep_delete(:subhash, :a) 341 | # #=> :val 342 | # x 343 | # #=> { :a => :val, :subhash => { :b => :val2 } } 344 | # 345 | def deep_delete *args 346 | last_key = args.pop # key to delete 347 | last_hsh = args.empty? ? self : (deep_get(*args)||{}) # hsh containing that key 348 | last_hsh.delete(last_key) 349 | end 350 | 351 | protected 352 | # @attr key The key to convert. 353 | # 354 | # @attr [Object] 355 | # The converted key. A dotted attr ('moon.cheese.type') becomes 356 | # an array of sequential keys for deep_set and deep_get 357 | # 358 | # @private 359 | def convert_key(attr) 360 | case 361 | when attr.to_s.include?('.') then attr.to_s.split(".").map{|sub_attr| sub_attr.to_sym } 362 | when attr.is_a?(Array) then attr.map{|sub_attr| sub_attr.to_sym } 363 | else attr.to_sym 364 | end 365 | end 366 | 367 | 368 | # @param value The value to convert. 369 | # 370 | # @return [Object] 371 | # The converted value. A Hash or an Array of hashes, will be converted to 372 | # their DeepHash equivalents. 373 | # 374 | # @private 375 | def convert_value(value) 376 | if value.class == Hash then self.class.new(value) 377 | elsif value.is_a?(Array) then value.collect{|e| convert_value(e) } 378 | else value 379 | end 380 | end 381 | 382 | end 383 | -------------------------------------------------------------------------------- /lib/configliere/define.rb: -------------------------------------------------------------------------------- 1 | module Configliere 2 | module Define 3 | 4 | # Define arbitrary attributes of a param, notably: 5 | # 6 | # [:description] Documentation for the param, used in the --help message 7 | # [:default] Sets a default value (applied immediately) 8 | # [:env_var] Environment variable to adopt (applied immediately, and after +:default+) 9 | # [:encrypted] Obscures/Extracts the contents of this param when serialized 10 | # [:type] Converts param's value to the given type, just before the finally block is called 11 | # [:finally] Block of code to postprocess settings or handle complex configuration. 12 | # [:required] Raises an error if, at the end of calling resolve!, the param's value is nil. 13 | # 14 | # @param param the setting to describe. Either a simple symbol or a dotted param string. 15 | # @param definitions the defineables to set (:description, :type, :encrypted, etc.) 16 | # 17 | # @example 18 | # Settings.define :dest_time, :type => Date, :description => 'Arrival time. If only a date is given, the current time of day on that date is assumed.' 19 | # Settings.define 'delorean.power_source', :description => 'Delorean subsytem supplying power to the Flux Capacitor.' 20 | # Settings.define :password, :required => true, :obscure => true 21 | # Settings.define :danger, :finally => lambda{|c| if c[:delorean][:power_source] == 'plutonium' than c.danger = 'high' } 22 | # 23 | def define param, pdefs={}, &block 24 | param = param.to_sym 25 | definitions[param].merge! pdefs 26 | self.use(:env_var) if pdefs.include?(:env_var) 27 | self.use(:encrypted) if pdefs.include?(:encrypted) 28 | self.use(:config_block) if pdefs.include?(:finally) 29 | self[param] = pdefs[:default] if pdefs.include?(:default) 30 | self.env_vars param => pdefs[:env_var] if pdefs.include?(:env_var) 31 | self.finally(&pdefs[:finally]) if pdefs.include?(:finally) 32 | self.finally(&block) if block 33 | self 34 | end 35 | 36 | # performs type coercion, continues up the resolve! chain 37 | def resolve! 38 | resolve_types! 39 | super() 40 | self 41 | end 42 | 43 | # ensures required types are defined, continues up the validate! chain 44 | def validate! 45 | validate_requireds! 46 | super() 47 | true 48 | end 49 | 50 | # =========================================================================== 51 | # 52 | # Helpers for retrieving definitions 53 | # 54 | 55 | private 56 | def definitions 57 | @definitions ||= Hash.new{|hsh, key| hsh[key.to_sym] = Hash.new } 58 | end 59 | public 60 | 61 | # Is the param defined? 62 | def has_definition?(param, attr=nil) 63 | if attr then definitions.has_key?(param.to_sym) && definitions[param].has_key?(attr) 64 | else definitions.has_key?(param.to_sym) end 65 | end 66 | 67 | # all params with a value for the given aspect 68 | # 69 | # @example 70 | # @config.define :has_description, :description => 'desc 1', :foo => 'bar' 71 | # # 72 | # definition_of(:has_description) 73 | # # => {:description => 'desc 1', :foo => 'bar'} 74 | # definition_of(:has_description, :description) 75 | # # => 'desc 1' 76 | # 77 | # @param aspect [Symbol] the aspect to list (:description, :type, :encrypted, etc.) 78 | # @return [Hash, Object] 79 | def definition_of(param, attr=nil) 80 | attr ? definitions[param.to_sym][attr] : definitions[param.to_sym] 81 | end 82 | 83 | # a hash holding every param with that aspect and its definition 84 | # 85 | # @example 86 | # @config.define :has_description, :description => 'desc 1' 87 | # @config.define :also_has_description, :description => 'desc 2' 88 | # @config.define :no_description, :something_else => 'foo' 89 | # # 90 | # params_with(:description) 91 | # # => { :has_description => 'desc 1', :also_has_description => 'desc 2' } 92 | # 93 | # @param aspect [Symbol] the aspect to list (:description, :type, :encrypted, etc.) 94 | # @return [Hash] 95 | def params_with(aspect) 96 | hsh = {} 97 | definitions.each do |param_name, param_def| 98 | next unless param_def.has_key?(aspect) 99 | hsh[param_name] = definition_of(param_name, aspect) 100 | end 101 | hsh 102 | end 103 | 104 | # =========================================================================== 105 | # 106 | # Type coercion 107 | # 108 | # Define types with 109 | # 110 | # Settings.define :param, :type => Date 111 | # 112 | 113 | # Coerce all params with types defined to their proper form 114 | def resolve_types! 115 | params_with(:type).each do |param, type| 116 | val = self[param] 117 | case 118 | when val.nil? then val = nil 119 | when (type == :boolean) then 120 | if ['false', false, 0, '0', ''].include?(val) then val = false else val = true end 121 | when (type == Array) 122 | if val.is_a?(String) then val = val.split(",") rescue nil ; end 123 | # for all following types, map blank/empty to nil 124 | when (val.respond_to?(:empty?) && val.empty?) then val = nil 125 | when (type == :filename) then val = File.expand_path(val) 126 | when (type == Float) then val = val.to_f 127 | when (type == Integer) then val = val.to_i 128 | when (type == Symbol) then val = val.to_s.to_sym rescue nil 129 | when (type == Regexp) then val = Regexp.new(val) rescue nil 130 | when ((val.to_s == 'now') && (type == Date)) then val = Date.today 131 | when ((val.to_s == 'now') && (type == DateTime)) then val = DateTime.now 132 | when ((val.to_s == 'now') && (type == Time)) then val = Time.now 133 | when [Date, Time, DateTime].include?(type) then val = type.parse(val) rescue nil 134 | else raise ArgumentError, "Unknown type #{type} given" 135 | end 136 | self[param] = val 137 | end 138 | end 139 | 140 | # =========================================================================== 141 | # 142 | # Required params 143 | # 144 | # Define requireds with 145 | # 146 | # Settings.define :param, :required => true 147 | # 148 | 149 | # Check that all required params are present. 150 | def validate_requireds! 151 | missing = [] 152 | params_with(:required).each do |param, is_required| 153 | missing << param if self[param].nil? && is_required 154 | end 155 | return if missing.empty? 156 | raise "Missing values for: #{missing.map{|pn| d = definition_of(pn, :description) ; (d ? "#{pn} (#{d})" : pn.to_s) }.sort.join(", ")}" 157 | end 158 | 159 | # Pretend that any #define'd parameter is a method 160 | # 161 | # @example 162 | # Settings.define :foo 163 | # Settings.foo = 4 164 | # Settings.foo #=> 4 165 | def method_missing meth, *args 166 | meth.to_s =~ /^(\w+)(=)?$/ or return super 167 | name, setter = [$1.to_sym, $2] 168 | return(super) unless has_definition?(name) 169 | if setter && (args.size == 1) 170 | self[name] = args.first 171 | elsif (!setter) && args.empty? 172 | self[name] 173 | else super ; end 174 | end 175 | 176 | end 177 | 178 | # Define is included by default 179 | Param.class_eval do 180 | include Configliere::Define 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/configliere/encrypted.rb: -------------------------------------------------------------------------------- 1 | require 'configliere/crypter' 2 | module Configliere 3 | module EncryptedParam 4 | 5 | # decrypts any encrypted params 6 | # then calls the next step in the resolve! chain. 7 | def resolve! 8 | resolve_encrypted! 9 | super() 10 | self 11 | end 12 | 13 | # import values, decrypting all params marked as encrypted 14 | def resolve_encrypted! 15 | remove_and_adopt_encrypt_pass_param_if_any! 16 | encrypted_params.each do |param| 17 | encrypted_val = deep_delete(*encrypted_key_path(param)) or next 18 | self[param] = self.decrypted(encrypted_val) 19 | end 20 | end 21 | 22 | protected 23 | 24 | # @example 25 | # Settings.defaults :username=>"mysql_username", :password=>"mysql_password" 26 | # Settings.define :password, :encrypted => true 27 | # Settings.export 28 | # #=> {:username => 'mysql_username', :password=>"\345?r`\222\021"\210\312\331\256\356\351\037\367\326" } 29 | def export 30 | remove_and_adopt_encrypt_pass_param_if_any! 31 | hsh = super() 32 | encrypted_params.each do |param| 33 | val = hsh.deep_delete(*convert_key(param)) or next 34 | hsh.deep_set( *(encrypted_key_path(param) | [encrypted(val)]) ) 35 | end 36 | hsh 37 | end 38 | 39 | # if :encrypted_pass was set as a param, remove it from the hash and set it as an attribute 40 | def remove_and_adopt_encrypt_pass_param_if_any! 41 | @encrypt_pass ||= self.delete(:encrypt_pass) if self[:encrypt_pass] 42 | @encrypt_pass ||= ENV['ENCRYPT_PASS'] if ENV['ENCRYPT_PASS'] 43 | end 44 | 45 | # the chain of symbol keys for a dotted path key, 46 | # prefixing the last one with "encrypted_" 47 | # 48 | # @example 49 | # encrypted_key_path('amazon.api.key') 50 | # #=> [:amazon, :api, :encrypted_key] 51 | def encrypted_key_path param 52 | encrypted_path = Array(convert_key(param)) 53 | encrypted_path[-1] = "encrypted_#{encrypted_path.last}".to_sym 54 | encrypted_path 55 | end 56 | 57 | # list of all params to encrypt on serialization 58 | def encrypted_params 59 | params_with(:encrypted).keys.select{|p| definition_of(p, :encrypted) } 60 | end 61 | 62 | def decrypted val 63 | return val.to_s if val.to_s.empty? 64 | Configliere::Crypter.decrypt(val, @encrypt_pass) 65 | end 66 | 67 | def encrypted val 68 | return unless val 69 | Configliere::Crypter.encrypt(val, @encrypt_pass) 70 | end 71 | end 72 | 73 | Param.on_use(:encrypted) do 74 | use :config_file, :define 75 | extend EncryptedParam 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/configliere/env_var.rb: -------------------------------------------------------------------------------- 1 | module Configliere 2 | # 3 | # EnvVar -- load configuration from environment variables 4 | # 5 | module EnvVar 6 | def env_vars *envs 7 | envs.each do |env| 8 | case env 9 | when Hash 10 | env.each do |env_param, env_var| 11 | adopt_env_var! env_param, env_var 12 | end 13 | else 14 | param = env.to_s.downcase.to_sym 15 | adopt_env_var! param, env 16 | end 17 | end 18 | end 19 | 20 | protected 21 | def adopt_env_var! param, env 22 | env = env.to_s 23 | definition_of(param)[:env_var] ||= env 24 | val = ENV[env] 25 | self[param] = val if val 26 | end 27 | end 28 | 29 | Param.on_use(:env_var) do 30 | use :commandline 31 | extend Configliere::EnvVar 32 | end 33 | end 34 | 35 | -------------------------------------------------------------------------------- /lib/configliere/param.rb: -------------------------------------------------------------------------------- 1 | module Configliere 2 | # 3 | # We want to be able to call super() on these methods in all included models, 4 | # so we define them in this parent shim class. 5 | # 6 | class ParamParent < DeepHash 7 | # default export method: dup of self 8 | def export 9 | dup.tap{|hsh| hsh.each{|k,v| hsh[k] = v.respond_to?(:export) ? v.export : v } } 10 | end 11 | 12 | # terminate resolution chain 13 | # @return self 14 | def resolve! 15 | self 16 | end 17 | 18 | # terminate validation chain. 19 | # @return self 20 | def validate! 21 | self 22 | end 23 | end 24 | 25 | # 26 | # Hash of fields to store. 27 | # 28 | # Any field name beginning with 'decrypted_' automatically creates a 29 | # counterpart 'encrypted_' field using the encrypt_pass. 30 | # 31 | class Param < Configliere::ParamParent 32 | 33 | # 34 | # Incorporates the given settings. 35 | # alias for deep_merge! 36 | # Existing values not given in the hash 37 | # 38 | # @param hsh the defaults to set. 39 | # 40 | # @example 41 | # Settings.defaults :hat => :cat, :basket => :lotion, :moon => { :man => :smiling } 42 | # Settings.defaults :basket => :tasket, :moon => { :cow => :smiling } 43 | # Config #=> { :hat => :cat, :basket => :tasket, :moon => { :man => :smiling, :cow => :jumping } } 44 | # 45 | # @return self 46 | def defaults hsh 47 | deep_merge! hsh 48 | self 49 | end 50 | 51 | # Finalize and validate params. All include'd modules and subclasses *must* call super() 52 | # @return self 53 | def resolve! 54 | super() 55 | validate! 56 | self 57 | end 58 | 59 | # Check that all defined params are valid. All include'd modules and subclasses *must*call super() 60 | # @return self 61 | def validate! 62 | super() 63 | self 64 | end 65 | 66 | def use *mws 67 | hsh = mws.pop if mws.last.is_a?(Hash) 68 | Configliere.use(*mws) 69 | mws.each do |mw| 70 | if (blk = USE_HANDLERS[mw]) 71 | instance_eval(&blk) 72 | end 73 | end 74 | self.deep_merge!(hsh) if hsh 75 | self 76 | end 77 | 78 | # @private 79 | USE_HANDLERS = {} unless defined?(USE_HANDLERS) 80 | # Block executed when use is invoked 81 | def self.on_use mw, &block 82 | USE_HANDLERS[mw] = block 83 | end 84 | 85 | def extractable_options? 86 | true 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/configliere/prompt.rb: -------------------------------------------------------------------------------- 1 | # Settings.use :prompt 2 | # you must install the highline gem 3 | 4 | begin 5 | require 'highline/import' 6 | rescue LoadError, NameError => err 7 | warn "************" 8 | warn "Highline does not work with JRuby 1.7.0+ as of Mid 2012. See https://github.com/JEG2/highline/issues/41." 9 | warn "************" 10 | raise 11 | end 12 | 13 | module Configliere 14 | # 15 | # Method to prompt for 16 | # 17 | module Prompt 18 | 19 | # Retrieve the given param, or prompt for it 20 | def prompt_for attr, hint=nil 21 | return self[attr] if has_key?(attr) 22 | hint ||= definition_of(attr, :description) 23 | hint = " (#{hint})" if hint 24 | self[attr] = ask("#{attr}#{hint}? ") 25 | end 26 | end 27 | 28 | Param.on_use(:prompt) do 29 | extend Configliere::Prompt 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/configliere/vayacondios.rb: -------------------------------------------------------------------------------- 1 | require 'vayacondios-client' 2 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.infochimps 5 | configliere 6 | jar 7 | 1.7.2-SNAPSHOT 8 | configliere 9 | http://maven.apache.org 10 | 11 | 12 | com.infochimps 13 | parent-pom 14 | 1.7.2-SNAPSHOT 15 | 16 | 17 | 18 | 19 | false 20 | 21 | 22 | 23 | 24 | 25 | infochimps.releases 26 | Infochimps Internal Repository 27 | https://s3.amazonaws.com/artifacts.chimpy.us/maven-s3p/releases 28 | 29 | 30 | infochimps.snapshots 31 | Infochimps Internal Repository 32 | https://s3.amazonaws.com/artifacts.chimpy.us/maven-s3p/snapshots 33 | 34 | true 35 | always 36 | 37 | 38 | 39 | 40 | 41 | 42 | com.infochimps 43 | vayacondios 44 | ${project.version} 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /spec/configliere/commandline_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../spec_helper', File.dirname(__FILE__)) 2 | 3 | describe "Configliere::Commandline" do 4 | before do 5 | @config = Configliere::Param.new :date => '11-05-1955', :cat => :hat 6 | @config.use :commandline 7 | end 8 | after do 9 | ::ARGV.replace [] 10 | end 11 | 12 | describe "with long-format argvs" do 13 | it 'accepts --param=val pairs' do 14 | ::ARGV.replace ['--enchantment=under_sea'] 15 | @config.resolve! 16 | @config.should == { :enchantment => 'under_sea', :date => '11-05-1955', :cat => :hat} 17 | end 18 | it 'accepts --dotted.param.name=val pairs as deep keys' do 19 | ::ARGV.replace ['--dotted.param.name=my_val'] 20 | @config.resolve! 21 | @config.rest.should be_empty 22 | @config.should == { :dotted => { :param => { :name => 'my_val' }}, :date => '11-05-1955', :cat => :hat } 23 | end 24 | it 'NO LONGER accepts --dashed-param-name=val pairs as deep keys' do 25 | ::ARGV.replace ['--dashed-param-name=my_val'] 26 | @config.should_receive(:warn).with("Configliere uses _underscores not dashes for params") 27 | @config.resolve! 28 | @config.rest.should be_empty 29 | @config.should == { :'dashed-param-name' => 'my_val', :date => '11-05-1955', :cat => :hat } 30 | end 31 | it 'adopts only the last-seen of duplicate commandline flags' do 32 | ::ARGV.replace ['--date=A', '--date=B'] 33 | @config.resolve! 34 | @config.rest.should be_empty 35 | @config.should == { :date => 'B', :cat => :hat} 36 | end 37 | it 'does NOT set a bare parameter (no "=") followed by a non-param to that value' do 38 | ::ARGV.replace ['--date', '11-05-1985', '--heavy', '--power.source', 'household waste', 'go'] 39 | @config.resolve! 40 | @config.rest.should == ['11-05-1985', 'household waste', 'go'] 41 | @config.should == { :date => true, :heavy => true, :power => { :source => true }, :cat => :hat } 42 | end 43 | it 'sets a bare parameter (no "=") to true' do 44 | ::ARGV.replace ['--date', '--deep.param'] 45 | @config.resolve! 46 | @config.rest.should be_empty 47 | @config.should == { :date => true, :deep => { :param => true }, :cat => :hat} 48 | end 49 | it 'sets an explicit blank to nil' do 50 | ::ARGV.replace ['--date=', '--deep.param='] 51 | @config.resolve! 52 | @config.should == { :date => nil, :deep => { :param => nil }, :cat => :hat} 53 | end 54 | 55 | it 'captures non --param args into Settings.rest' do 56 | ::ARGV.replace ['--date', 'file1', 'file2'] 57 | @config.resolve! 58 | @config.should == { :date => true, :cat => :hat} 59 | @config.rest.should == ['file1', 'file2'] 60 | end 61 | 62 | it 'stops processing args on "--"' do 63 | ::ARGV.replace ['--date=A', '--', '--date=B'] 64 | @config.resolve! 65 | @config.rest.should == ['--date=B'] 66 | @config.should == { :date => 'A', :cat => :hat} 67 | end 68 | 69 | it 'places undefined argvs into #unknown_argvs' do 70 | @config.define :raven, :description => 'squawk' 71 | ::ARGV.replace ['--never=more', '--lenore', '--raven=ray_lewis'] 72 | @config.resolve! 73 | @config.unknown_argvs.should == [:never, :lenore] 74 | @config.should == { :date => '11-05-1955', :cat => :hat, :never => 'more', :lenore => true, :raven => 'ray_lewis' } 75 | end 76 | end 77 | 78 | describe "with single-letter flags" do 79 | before do 80 | @config.define :date, :flag => :d 81 | @config.define :cat, :flag => 'c' 82 | @config.define :process, :flag => :p 83 | end 84 | 85 | it 'accepts them separately' do 86 | ::ARGV.replace ['-p', '-c'] 87 | @config.resolve! 88 | @config.rest.should == [] 89 | @config.should == { :date => '11-05-1955', :cat => true, :process => true} 90 | end 91 | 92 | it 'accepts them as a group ("-abc")' do 93 | ::ARGV.replace ['-pc'] 94 | @config.resolve! 95 | @config.rest.should == [] 96 | @config.should == { :date => '11-05-1955', :cat => true, :process => true} 97 | end 98 | 99 | it 'accepts a value with -d=new_val' do 100 | ::ARGV.replace ['-d=new_val', '-c'] 101 | @config.resolve! 102 | @config.rest.should == [] 103 | @config.should == { :date => 'new_val', :cat => true } 104 | end 105 | 106 | it 'accepts a space-separated value (-d new_val)' do 107 | ::ARGV.replace ['-d', 'new_val', '-c', '-p'] 108 | @config.resolve! 109 | @config.rest.should == [] 110 | @config.should == { :date => 'new_val', :cat => true, :process => true } 111 | end 112 | 113 | it 'accepts a space-separated value only if the next arg is not a flag' do 114 | ::ARGV.replace ['-d', 'new_val', '-c', '-p', 'vigorously'] 115 | @config.resolve! 116 | @config.rest.should == [] 117 | @config.should == { :date => 'new_val', :cat => true, :process => 'vigorously' } 118 | end 119 | 120 | it 'stores unknown single-letter flags in unknown_argvs' do 121 | ::ARGV.replace ['-dcz'] 122 | lambda{ @config.resolve! }.should_not raise_error(Configliere::Error) 123 | @config.should == { :date => true, :cat => true } 124 | @config.unknown_argvs.should == ['z'] 125 | end 126 | end 127 | 128 | def capture_help_message 129 | stderr_output = '' 130 | @config.should_receive(:warn){|str| stderr_output << str } 131 | begin 132 | yield 133 | fail('should exit via system exit') 134 | rescue SystemExit 135 | true # pass 136 | end 137 | stderr_output 138 | end 139 | 140 | describe "the help message" do 141 | it 'displays help' do 142 | ::ARGV.replace ['--help'] 143 | stderr_output = capture_help_message{ @config.resolve! } 144 | stderr_output.should_not be_nil 145 | stderr_output.should_not be_empty 146 | 147 | @config.help.should_not be_nil 148 | @config.help.should_not be_empty 149 | end 150 | 151 | it "displays the single-letter flags" do 152 | @config.define :cat, :flag => :c, :description => "I like single-letter commands." 153 | ::ARGV.replace ['--help'] 154 | stderr_output = capture_help_message{ @config.resolve! } 155 | stderr_output.should match(/-c,/m) 156 | end 157 | 158 | it "displays command line options" do 159 | ::ARGV.replace ['--help'] 160 | 161 | @config.define :logfile, :type => String, :description => "Log file name", :default => 'myapp.log', :required => false 162 | @config.define :debug, :type => :boolean, :description => "Log debug messages to console?", :required => false 163 | @config.define :dest_time, :type => DateTime, :description => "Arrival time", :required => true 164 | @config.define :takes_opt, :flag => 't', :description => "Takes a single-letter flag '-t'" 165 | @config.define :foobaz, :internal => true, :description => "You won't see me" 166 | @config.define :password, :required => true, :encrypted => true 167 | @config.define 'delorean.power_source', :env_var => 'POWER_SOURCE', :description => 'Delorean subsytem supplying power to the Flux Capacitor.' 168 | @config.description = 'This is a sample script to demonstrate the help message. Notice how pretty everything lines up YAY' 169 | 170 | stderr_output = capture_help_message{ @config.resolve! } 171 | stderr_output.should_not be_nil 172 | stderr_output.should_not be_empty 173 | 174 | stderr_output.should =~ %r{--debug\s}s # type :boolean 175 | stderr_output.should =~ %r{--logfile=String\s}s # type String 176 | stderr_output.should =~ %r{--dest_time=DateTime[^\n]+\[Required\]}s # shows required params 177 | stderr_output.should =~ %r{--password=String[^\n]+\[Encrypted\]}s # shows encrypted params 178 | stderr_output.should =~ %r{--delorean.power_source=String\s}s # undefined type 179 | stderr_output.should =~ %r{--password=String\s*password}s # uses name as dummy description 180 | stderr_output.should =~ %r{-t, --takes_opt}s # single-letter flags 181 | 182 | stderr_output.should =~ %r{delorean\.power_source[^\n]+Env Var: POWER_SOURCE}s # environment variable 183 | stderr_output.should =~ %r{This is a sample script}s # extra description 184 | end 185 | 186 | it 'lets me die' do 187 | stderr_output = '' 188 | @config.should_receive(:dump_help).with("****\nhi mom\n****") 189 | @config.should_receive(:exit).with(3) 190 | @config.die("hi mom", 3) 191 | end 192 | end 193 | 194 | describe 'recycling a commandline' do 195 | it 'exports dashed flags' do 196 | @config.define :has_underbar, :type => Integer, :default => 1 197 | @config.define :not_here, :type => Integer 198 | @config.define :is_truthy, :type => :boolean, :default => true 199 | @config.define :is_falsehood, :type => :boolean, :default => false 200 | @config.dashed_flags(:has_underbar, :not_here, :is_truthy, :is_falsehood, :date, :cat 201 | ).should == ["--has-underbar=1", "--is-truthy", "--date=11-05-1955", "--cat=hat"] 202 | end 203 | end 204 | 205 | describe '#resolve!' do 206 | it 'calls super and returns self' do 207 | Configliere::ParamParent.class_eval do def resolve!() dummy ; end ; end 208 | @config.should_receive(:dummy) 209 | @config.resolve!.should equal(@config) 210 | Configliere::ParamParent.class_eval do def resolve!() self ; end ; end 211 | end 212 | end 213 | 214 | describe '#validate!' do 215 | it 'calls super and returns self' do 216 | Configliere::ParamParent.class_eval do def validate!() dummy ; end ; end 217 | @config.should_receive(:dummy) 218 | @config.validate!.should equal(@config) 219 | Configliere::ParamParent.class_eval do def validate!() self ; end ; end 220 | end 221 | end 222 | 223 | end 224 | -------------------------------------------------------------------------------- /spec/configliere/commands_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../spec_helper', File.dirname(__FILE__)) 2 | require 'configliere/commands' 3 | 4 | describe Configliere::Commands do 5 | 6 | subject{ Configliere::Param.new.use(:commands) } 7 | 8 | after{ ::ARGV.replace [] } 9 | 10 | context 'when no commands are defined' do 11 | 12 | its(:commands){ should be_empty } 13 | 14 | let(:args) { %w[ not_command_but_arg another_arg ] } 15 | 16 | it 'does not shift the ARGV when resolving' do 17 | ::ARGV.replace args 18 | subject.resolve! 19 | subject.rest.should == args 20 | subject.command_name.should be_nil 21 | subject.command_info.should be_nil 22 | end 23 | 24 | it 'still recognize a git-style-binary command' do 25 | ::ARGV.replace args 26 | File.should_receive(:basename).and_return('prog-subcommand') 27 | subject.resolve! 28 | subject.rest.should == args 29 | subject.command_name.should == :subcommand 30 | subject.command_info.should be_nil 31 | end 32 | end 33 | 34 | context 'a simple command' do 35 | let(:args) { %w[ the_command --fuzziness=wuzzy extra_arg ] } 36 | 37 | before do 38 | subject.defaults(:fuzziness => 'smooth') 39 | subject.define_command(:the_command, :description => 'foobar') 40 | end 41 | 42 | it "should continue to parse flags when the command is given" do 43 | ::ARGV.replace args 44 | subject.resolve! 45 | subject.should == { :fuzziness => 'wuzzy' } 46 | end 47 | 48 | it "should continue to set args when the command is given" do 49 | ::ARGV.replace args 50 | subject.resolve! 51 | subject.rest.should == ['extra_arg'] 52 | end 53 | 54 | it "should recognize the command when given" do 55 | ::ARGV.replace args 56 | subject.resolve! 57 | subject.command_name.should == :the_command 58 | subject.command_info.should == { :description => "foobar", :config => { :fuzziness => 'wuzzy' } } 59 | end 60 | 61 | it "should recognize when the command is not given" do 62 | ::ARGV.replace ['bogus_command', '--fuzziness=wuzzy', 'an_arg'] 63 | subject.resolve! 64 | subject.rest.should == ['bogus_command', 'an_arg'] 65 | subject.command_name.should be_nil 66 | end 67 | end 68 | 69 | describe "a complex command" do 70 | before do 71 | subject.defaults :outer => 'val 1' 72 | subject.define_command "the_command", :description => "the command" do |cmd| 73 | cmd.define :inner, :description => "inside" 74 | end 75 | end 76 | 77 | it "should still recognize the outer param and the args" do 78 | ::ARGV.replace ['the_command', '--outer=wuzzy', 'an_arg', '--inner=buzz'] 79 | subject.resolve! 80 | subject.rest.should == ['an_arg'] 81 | subject.command_name.should == :the_command 82 | subject[:outer].should == 'wuzzy' 83 | end 84 | 85 | it "should recognize the inner param" do 86 | ::ARGV.replace ['the_command', '--outer=wuzzy', 'an_arg', '--inner=buzz'] 87 | subject.resolve! 88 | subject[:inner].should == 'buzz' 89 | subject.command_info[:config][:inner].should == 'buzz' 90 | end 91 | end 92 | 93 | describe "the help message" do 94 | before do 95 | subject.define_command :run, :description => "forrest" 96 | subject.define_command :stop, :description => "hammertime" 97 | subject.define :reel, :type => Integer 98 | end 99 | 100 | it "displays a modified usage" do 101 | ::ARGV.replace ['--help'] 102 | stderr_output = capture_help_message{ subject.resolve! } 103 | stderr_output.should =~ %r{usage:.*\[command\]}m 104 | end 105 | 106 | it "displays the commands and their descriptions", :if => (RUBY_VERSION < "2.0") do 107 | ::ARGV.replace ['--help'] 108 | stderr_output = capture_help_message{ subject.resolve! } 109 | stderr_output.should =~ %r{Available commands:\s+run\s*forrest\s+stop\s+hammertime}m 110 | stderr_output.should =~ %r{Params:.*--reel=Integer\s+reel}m 111 | end 112 | end 113 | 114 | describe '#resolve!' do 115 | it 'calls super and returns self' do 116 | Configliere::ParamParent.class_eval do def resolve!() dummy ; end ; end 117 | subject.should_receive(:dummy) 118 | subject.resolve!.should equal(subject) 119 | Configliere::ParamParent.class_eval do def resolve!() self ; end ; end 120 | end 121 | end 122 | 123 | describe '#validate!' do 124 | it 'calls super and returns self' do 125 | Configliere::ParamParent.class_eval do def validate!() dummy ; end ; end 126 | subject.should_receive(:dummy) 127 | subject.validate!.should equal(subject) 128 | Configliere::ParamParent.class_eval do def validate!() self ; end ; end 129 | end 130 | end 131 | 132 | end 133 | -------------------------------------------------------------------------------- /spec/configliere/config_block_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../spec_helper', File.dirname(__FILE__)) 2 | Configliere.use :config_block 3 | 4 | describe "Configliere::ConfigBlock" do 5 | before do 6 | @config = Configliere::Param.new :normal_param => 'normal' 7 | end 8 | 9 | describe 'resolving' do 10 | it 'runs blocks' do 11 | @block_watcher = 'watcher' 12 | # @block_watcher.should_receive(:fnord).with(@config) 13 | @block_watcher.should_receive(:fnord) 14 | @config.finally{|arg| @block_watcher.fnord(arg) } 15 | @config.resolve! 16 | end 17 | it 'resolves blocks last' do 18 | Configliere.use :config_block, :encrypted 19 | @config.should_receive(:resolve_types!).ordered 20 | @config.should_receive(:resolve_finally_blocks!).ordered 21 | @config.resolve! 22 | end 23 | 24 | it 'calls super and returns self' do 25 | Configliere::ParamParent.class_eval do def resolve!() dummy ; end ; end 26 | @config.should_receive(:dummy) 27 | @config.resolve!.should equal(@config) 28 | Configliere::ParamParent.class_eval do def resolve!() self ; end ; end 29 | end 30 | end 31 | 32 | describe '#validate!' do 33 | it 'calls super and returns self' do 34 | Configliere::ParamParent.class_eval do def validate!() dummy ; end ; end 35 | @config.should_receive(:dummy) 36 | @config.validate!.should equal(@config) 37 | Configliere::ParamParent.class_eval do def validate!() self ; end ; end 38 | end 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /spec/configliere/config_file_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../spec_helper', File.dirname(__FILE__)) 2 | 3 | describe Configliere::ConfigFile do 4 | let(:default_params) { { :my_param => 'default_val', :also_a_param => true } } 5 | 6 | subject{ Configliere::Param.new default_params } 7 | 8 | it 'is included by default' do 9 | subject.class.included_modules.should include(described_class) 10 | end 11 | 12 | context '#read' do 13 | let(:file_params) { { :my_param => 'val_from_file' } } 14 | let(:file_string) { file_params.to_yaml } 15 | let(:file_path) { '/absolute/path.yaml' } 16 | 17 | before{ File.stub(:open).and_return(file_string) } 18 | 19 | it 'returns the config object for chaining' do 20 | subject.read(file_path).should == subject 21 | end 22 | 23 | context 'a yaml file' do 24 | let(:file_path) { '/absolute/path.yaml' } 25 | 26 | it 'reads successfully' do 27 | subject.should_receive(:read_yaml).with(file_string, {}) 28 | subject.read file_path 29 | end 30 | 31 | it 'merges the data' do 32 | subject.read(file_path).should == default_params.merge(file_params) 33 | end 34 | end 35 | 36 | context 'a json file' do 37 | let(:file_path) { '/absolute/path.json' } 38 | let(:file_string) { '{"my_param":"val_from_file"}' } 39 | 40 | it 'reads successfully' do 41 | subject.should_receive(:read_json).with(file_string, {}) 42 | subject.read file_path 43 | end 44 | 45 | it 'merges the data' do 46 | subject.read(file_path).should == default_params.merge(file_params) 47 | end 48 | end 49 | 50 | context 'given a symbol' do 51 | let(:file_path) { :my_settings } 52 | 53 | it 'no longer provides a default config file' do 54 | expect{ subject.read(file_path) }.to raise_error(Configliere::DeprecatedError) 55 | defined?(Configliere::DEFAULT_CONFIG_FILE).should_not be_true 56 | end 57 | end 58 | 59 | context 'given a nonexistent file' do 60 | let(:file_path) { 'nonexistent.conf' } 61 | 62 | it 'warns but does not fail if the file is missing' do 63 | File.stub(:open).and_raise(Errno::ENOENT) 64 | subject.should_receive(:warn).with("Loading empty configliere settings file #{subject.default_conf_dir}/#{file_path}") 65 | subject.read(file_path).should == subject 66 | end 67 | end 68 | 69 | context 'given an absolute path' do 70 | let(:file_path) { '/absolute/path.yaml' } 71 | 72 | it 'uses it directly' do 73 | File.should_receive(:open).with(file_path).and_return(file_string) 74 | subject.read file_path 75 | end 76 | end 77 | 78 | context 'given a simple filename' do 79 | let(:file_path) { 'simple_path.yaml' } 80 | 81 | it 'references it to the default config dir' do 82 | File.should_receive(:open).with(File.join(subject.default_conf_dir, file_path)).and_return(file_string) 83 | subject.read file_path 84 | end 85 | end 86 | 87 | context 'with options' do 88 | let(:file_params) { { :development => { :reload => true }, :production => { :reload => false } } } 89 | 90 | before{ subject.merge!(:reload => 'whatever') } 91 | 92 | context ':env key' do 93 | context 'valid :env' do 94 | let(:opts) { { :env => :development } } 95 | 96 | it 'slices out a subhash given by :env' do 97 | subject.read(file_path, opts) 98 | subject.should == default_params.merge(:reload => true) 99 | end 100 | end 101 | 102 | context 'invalid :env' do 103 | let(:opts) { { :env => :not_there } } 104 | 105 | it 'has no effect if the key given by :env option is absent' do 106 | subject.read(file_path, opts) 107 | subject.should == default_params.merge(:reload => 'whatever') 108 | end 109 | end 110 | end 111 | 112 | context 'no :env key' do 113 | let(:opts) { Hash.new } 114 | 115 | it 'does no slicing without the :env option' do 116 | subject.read(file_path, opts) 117 | subject.should == default_params.merge(:reload => 'whatever').merge(file_params) 118 | end 119 | end 120 | end 121 | end 122 | 123 | context '#save!' do 124 | let(:fake_file) { StringIO.new('', 'w') } 125 | 126 | context 'given an absolute pathname' do 127 | let(:file_path) { '/absolute/path.yaml' } 128 | 129 | it 'saves the filename as given' do 130 | File.should_receive(:open).with(file_path, 'w').and_yield(fake_file) 131 | FileUtils.stub(:mkdir_p) 132 | fake_file.should_receive(:<<).with(default_params.to_yaml) 133 | subject.save! file_path 134 | end 135 | end 136 | 137 | context 'given a simple pathname' do 138 | let(:file_path) { 'simple_path.yaml' } 139 | 140 | it 'saves the filename in the default config dir' do 141 | File.should_receive(:open).with(File.join(subject.default_conf_dir, file_path), 'w').and_yield(fake_file) 142 | fake_file.should_receive(:<<).with(default_params.to_yaml) 143 | subject.save! file_path 144 | end 145 | 146 | it 'ensures the directory exists' do 147 | File.should_receive(:open).with(File.join(subject.default_conf_dir, file_path), 'w').and_yield(fake_file) 148 | FileUtils.should_receive(:mkdir_p).with(subject.default_conf_dir.to_s) 149 | subject.save! file_path 150 | end 151 | end 152 | end 153 | 154 | context '#resolve!' do 155 | around do |example| 156 | Configliere::ParamParent.class_eval{ def resolve!() parent_method ; end } 157 | example.run 158 | Configliere::ParamParent.class_eval{ def resolve!() self ; end } 159 | end 160 | 161 | it 'calls super and returns self' do 162 | subject.should_receive(:parent_method) 163 | subject.resolve!.should equal(subject) 164 | end 165 | end 166 | 167 | describe '#validate!' do 168 | around do |example| 169 | Configliere::ParamParent.class_eval{ def validate!() parent_method ; end } 170 | example.run 171 | Configliere::ParamParent.class_eval{ def validate!() self ; end } 172 | end 173 | 174 | it 'calls super and returns self' do 175 | subject.should_receive(:parent_method) 176 | subject.validate!.should equal(subject) 177 | end 178 | end 179 | 180 | context '#load_configuration_in_order!' do 181 | let(:scope) { 'test' } 182 | 183 | before{ subject.stub(:determine_conf_location).and_return('conf_dir') } 184 | 185 | it 'resolves configuration in order' do 186 | subject.should_receive(:determine_conf_location).with(:machine, scope).ordered 187 | subject.should_receive(:determine_conf_location).with(:user, scope).ordered 188 | subject.should_receive(:determine_conf_location).with(:app, scope).ordered 189 | subject.should_receive(:resolve!).ordered 190 | subject.load_configuration_in_order!(scope) 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /spec/configliere/deep_hash_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | class AwesomeHash < DeepHash ; end 4 | 5 | describe DeepHash do 6 | before(:each) do 7 | @deep_hash = DeepHash.new({ :nested_1 => { :nested_2 => { :leaf_3 => "val3" }, :leaf_2 => ['arr'] }, :leaf_at_top => 'val1b' }) 8 | @hash = { "str_key" => "strk_val", :sym_key => "symk_val"} 9 | @sub = AwesomeHash.new("str_key" => "strk_val", :sym_key => "symk_val") 10 | end 11 | 12 | describe "#initialize" do 13 | it 'adopts a Hash when given' do 14 | deep_hash = DeepHash.new(@hash) 15 | @hash.each{|k,v| deep_hash[k].should == v } 16 | deep_hash.keys.any?{|key| key.is_a?(String) }.should be_false 17 | end 18 | 19 | it 'converts all pure Hash values into DeepHashes if param is a Hash' do 20 | deep_hash = DeepHash.new :sym_key => @hash 21 | deep_hash[:sym_key].should be_an_instance_of(DeepHash) 22 | # sanity check 23 | deep_hash[:sym_key][:sym_key].should == "symk_val" 24 | end 25 | 26 | it 'does not convert Hash subclass values into DeepHashes' do 27 | deep_hash = DeepHash.new :sub => @sub 28 | deep_hash[:sub].should be_an_instance_of(AwesomeHash) 29 | end 30 | 31 | it 'converts all value items if value is an Array' do 32 | deep_hash = DeepHash.new :arry => { :sym_key => [@hash] } 33 | deep_hash[:arry].should be_an_instance_of(DeepHash) 34 | # sanity check 35 | deep_hash[:arry][:sym_key].first[:sym_key].should == "symk_val" 36 | end 37 | 38 | it 'delegates to superclass constructor if param is not a Hash' do 39 | deep_hash = DeepHash.new("dash berlin") 40 | 41 | deep_hash["unexisting key"].should == "dash berlin" 42 | end 43 | end # describe "#initialize" 44 | 45 | describe "#update" do 46 | it 'converts all keys into symbols when param is a Hash' do 47 | deep_hash = DeepHash.new(@hash) 48 | deep_hash.update("starry" => "night") 49 | 50 | deep_hash.keys.any?{|key| key.is_a?(String) }.should be_false 51 | end 52 | 53 | it 'converts all Hash values into DeepHashes if param is a Hash' do 54 | deep_hash = DeepHash.new :hash => @hash 55 | deep_hash.update(:hash => { :sym_key => "is buggy in Ruby 1.8.6" }) 56 | 57 | deep_hash[:hash].should be_an_instance_of(DeepHash) 58 | end 59 | end # describe "#update" 60 | 61 | describe '#[]=' do 62 | it 'symbolizes keys' do 63 | @deep_hash['leaf_at_top'] = :fedora 64 | @deep_hash['new'] = :unseen 65 | @deep_hash.should == {:nested_1 => {:nested_2 => {:leaf_3 => "val3"}, :leaf_2 => ['arr']}, :leaf_at_top => :fedora, :new => :unseen} 66 | end 67 | it 'deep-sets dotted vals, replacing values' do 68 | @deep_hash['moon.man'] = :cheesy 69 | @deep_hash[:moon][:man].should == :cheesy 70 | end 71 | it 'deep-sets dotted vals, creating new values' do 72 | @deep_hash['moon.cheese.type'] = :tilsit 73 | @deep_hash[:moon][:cheese][:type].should == :tilsit 74 | end 75 | it 'deep-sets dotted vals, auto-vivifying intermediate hashes' do 76 | @deep_hash['this.that.the_other'] = :fuhgeddaboudit 77 | @deep_hash[:this][:that][:the_other].should == :fuhgeddaboudit 78 | end 79 | it 'converts all Hash value into DeepHash' do 80 | deep_hash = DeepHash.new :hash => @hash 81 | deep_hash[:hash] = { :sym_key => "is buggy in Ruby 1.8.6" } 82 | deep_hash[:hash].should be_an_instance_of(DeepHash) 83 | end 84 | 85 | it "only accepts #to_sym'bolizable things as keys" do 86 | lambda{ @deep_hash[{ :a => :b }] = 'hi' }.should raise_error(NoMethodError, /undefined method `to_sym'/) 87 | lambda{ @deep_hash[Object.new] = 'hi' }.should raise_error(NoMethodError, /undefined method `to_sym'/) 88 | lambda{ @deep_hash[:a] = 'hi' }.should_not raise_error 89 | end 90 | end 91 | 92 | describe '#[]' do 93 | let(:orig_hash){ { :hat => :cat, :basket => :lotion, :moon => { :man => :smiling, :cheese => {:type => :tilsit} } } } 94 | let(:subject ){ Configliere::Param.new({ :hat => :cat, :basket => :lotion, :moon => { :man => :smiling, :cheese => {:type => :tilsit} } }) } 95 | 96 | it 'deep-gets dotted vals' do 97 | subject['moon.man'].should == :smiling 98 | subject['moon.cheese.type'].should == :tilsit 99 | subject['moon.cheese.smell'].should be_nil 100 | subject['moon.non.existent.interim.values'].should be_nil 101 | subject['moon.non'].should be_nil 102 | subject.should == orig_hash # shouldn't change from reading (specifically, shouldn't autovivify) 103 | end 104 | it 'indexing through a non-hash will raise an error', :if => (defined?(RUBY_ENGINE) && (RUBY_ENGINE !~ /rbx/)) do 105 | err_klass = (RUBY_VERSION >= "1.9.0") ? TypeError : NoMethodError 106 | expect{ subject['hat.dog'] }.to raise_error(err_klass, /Symbol/) 107 | subject.should == orig_hash # shouldn't change from reading (specifically, shouldn't autovivify) 108 | end 109 | 110 | it "only accepts #to_sym'bolizable things as keys" do 111 | lambda{ subject[{ :a => :b }] }.should raise_error(NoMethodError, /undefined method `to_sym'/) 112 | lambda{ subject[Object.new] }.should raise_error(NoMethodError, /undefined method `to_sym'/) 113 | end 114 | end 115 | 116 | def arrays_should_be_equal arr1, arr2 117 | arr1.sort_by{|s| s.to_s }.should == arr2.sort_by{|s| s.to_s } 118 | end 119 | 120 | describe "#to_hash" do 121 | it 'returns instance of Hash' do 122 | DeepHash.new(@hash).to_hash.should be_an_instance_of(Hash) 123 | end 124 | 125 | it 'preserves keys' do 126 | deep_hash = DeepHash.new(@hash) 127 | converted = deep_hash.to_hash 128 | arrays_should_be_equal deep_hash.keys, converted.keys 129 | end 130 | 131 | it 'preserves value' do 132 | deep_hash = DeepHash.new(@hash) 133 | converted = deep_hash.to_hash 134 | arrays_should_be_equal deep_hash.values, converted.values 135 | end 136 | end 137 | 138 | describe '#compact' do 139 | it 'removes nils but not empties or falsehoods' do 140 | DeepHash.new({ :a => nil }).compact.should == {} 141 | DeepHash.new({ :a => nil, :b => false, :c => {}, :d => "", :remains => true }).compact.should == { :b => false, :c => {}, :d => "", :remains => true } 142 | end 143 | 144 | it 'leaves original alone' do 145 | deep_hash = DeepHash.new({ :a => nil, :remains => true }) 146 | deep_hash.compact.should == { :remains => true } 147 | deep_hash.should == { :a => nil, :remains => true } 148 | end 149 | end 150 | 151 | describe '#compact!' do 152 | it 'removes nils but not empties or falsehoods' do 153 | DeepHash.new({ :a => nil}).compact!.should == {} 154 | DeepHash.new({ :a => nil, :b => false, :c => {}, :d => "", :remains => true }).compact!.should == { :b => false, :c => {}, :d => "", :remains => true } 155 | end 156 | 157 | it 'modifies in-place' do 158 | deep_hash = DeepHash.new({ :a => nil, :remains => true }) 159 | deep_hash.compact!.should == { :remains => true } 160 | deep_hash.should == { :remains => true } 161 | end 162 | end 163 | 164 | describe '#slice' do 165 | let(:subject ){ Configliere::Param.new({ :a => 'x', :b => 'y', :c => 10 }) } 166 | 167 | it 'returns a new hash with only the given keys' do 168 | subject.slice(:a, :b).should == { :a => 'x', :b => 'y' } 169 | subject.should == { :a => 'x', :b => 'y', :c => 10 } 170 | end 171 | 172 | it 'with bang replaces the hash with only the given keys' do 173 | subject.slice!(:a, :b).should == { :c => 10 } 174 | subject.should == { :a => 'x', :b => 'y' } 175 | end 176 | 177 | it 'ignores an array key' do 178 | subject.slice([:a, :b], :c).should == { :c => 10 } 179 | subject.should == { :a => 'x', :b => 'y', :c => 10 } 180 | end 181 | 182 | it 'with bang ignores an array key' do 183 | subject.slice!([:a, :b], :c).should == { :a => 'x', :b => 'y' } 184 | subject.should == { :c => 10 } 185 | end 186 | 187 | it 'uses splatted keys individually' do 188 | subject.slice(*[:a, :b]).should == { :a => 'x', :b => 'y' } 189 | subject.should == { :a => 'x', :b => 'y', :c => 10 } 190 | end 191 | 192 | it 'with bank uses splatted keys individually' do 193 | subject.slice!(*[:a, :b]).should == { :c => 10 } 194 | subject.should == { :a => 'x', :b => 'y' } 195 | end 196 | end 197 | 198 | describe '#extract' do 199 | let(:subject ){ Configliere::Param.new({ :a => 'x', :b => 'y', :c => 10 }) } 200 | 201 | it 'replaces the hash with only the given keys' do 202 | subject.extract!(:a, :b).should == { :a => 'x', :b => 'y' } 203 | subject.should == { :c => 10 } 204 | end 205 | 206 | it 'leaves the hash empty if all keys are gone' do 207 | subject.extract!(:a, :b, :c).should == { :a => 'x', :b => 'y', :c => 10 } 208 | subject.should == {} 209 | end 210 | 211 | it 'gets values for all given keys even if missing' do 212 | subject.extract!(:bob, :c).should == { :bob => nil, :c => 10 } 213 | subject.should == { :a => 'x', :b => 'y' } 214 | end 215 | 216 | it 'is OK when empty' do 217 | DeepHash.new.slice!(:a, :b, :c).should == {} 218 | end 219 | 220 | it 'returns an instance of the same class' do 221 | subject.slice(:a).should be_a(DeepHash) 222 | end 223 | end 224 | 225 | describe "#delete" do 226 | it 'converts Symbol key into String before deleting' do 227 | deep_hash = DeepHash.new(@hash) 228 | 229 | deep_hash.delete(:sym_key) 230 | deep_hash.key?("hash").should be_false 231 | end 232 | 233 | it 'works with String keys as well' do 234 | deep_hash = DeepHash.new(@hash) 235 | 236 | deep_hash.delete("str_key") 237 | deep_hash.key?("str_key").should be_false 238 | end 239 | end 240 | 241 | describe "#fetch" do 242 | let(:subject ){ Configliere::Param.new({ :no => :in_between }) } 243 | 244 | it 'converts key before fetching' do 245 | subject.fetch("no").should == :in_between 246 | end 247 | 248 | it 'returns alternative value if key lookup fails' do 249 | subject.fetch("flying", "screwdriver").should == "screwdriver" 250 | end 251 | end 252 | 253 | describe "#values_at" do 254 | 255 | let(:subject ){ Configliere::Param.new({ :no => :in_between, "str_key" => "strk_val", :sym_key => "symk_val"}) } 256 | 257 | it 'is indifferent to whether keys are strings or symbols' do 258 | subject.values_at("sym_key", :str_key, :no).should == ["symk_val", "strk_val", :in_between] 259 | end 260 | end 261 | 262 | it 'responds to #symbolize_keys, #symbolize_keys! and #stringify_keys but not #stringify_keys!' do 263 | DeepHash.new.should respond_to(:symbolize_keys ) 264 | DeepHash.new.should respond_to(:symbolize_keys!) 265 | DeepHash.new.should respond_to(:stringify_keys ) 266 | DeepHash.new.should_not respond_to(:stringify_keys!) 267 | end 268 | 269 | describe '#symbolize_keys' do 270 | it 'returns a dup of itself' do 271 | deep_hash = DeepHash.new(@hash) 272 | deep_hash.symbolize_keys.should == deep_hash 273 | end 274 | end 275 | 276 | describe '#symbolize_keys!' do 277 | it 'with bang returns the deep_hash itself' do 278 | deep_hash = DeepHash.new(@hash) 279 | deep_hash.symbolize_keys!.object_id.should == deep_hash.object_id 280 | end 281 | end 282 | 283 | describe '#stringify_keys' do 284 | it 'converts keys that are all symbols' do 285 | @deep_hash.stringify_keys.should == 286 | { 'nested_1' => { :nested_2 => { :leaf_3 => "val3" }, :leaf_2 => ['arr'] }, 'leaf_at_top' => 'val1b' } 287 | end 288 | 289 | it 'returns a Hash, not a DeepHash' do 290 | @deep_hash.stringify_keys.class.should == Hash 291 | @deep_hash.stringify_keys.should_not be_a(DeepHash) 292 | end 293 | 294 | it 'only stringifies and hashifies the top level' do 295 | stringified = @deep_hash.stringify_keys 296 | stringified.should == { 'nested_1' => { :nested_2 => { :leaf_3 => "val3" }, :leaf_2 => ['arr'] }, 'leaf_at_top' => 'val1b' } 297 | stringified['nested_1'].should be_a(DeepHash) 298 | end 299 | end 300 | 301 | describe '#assert_valid_keys' do 302 | before do 303 | @deep_hash = DeepHash.new({ :failure => "stuff", :funny => "business" }) 304 | end 305 | 306 | it 'is true and does not raise when valid' do 307 | @deep_hash.assert_valid_keys([ :failure, :funny ]).should be_nil 308 | @deep_hash.assert_valid_keys(:failure, :funny).should be_nil 309 | end 310 | 311 | it 'fails when invalid' do 312 | @deep_hash[:failore] = @deep_hash.delete(:failure) 313 | lambda{ @deep_hash.assert_valid_keys([ :failure, :funny ]) }.should raise_error(ArgumentError, "Unknown key(s): failore") 314 | lambda{ @deep_hash.assert_valid_keys(:failure, :funny) }.should raise_error(ArgumentError, "Unknown key(s): failore") 315 | end 316 | end 317 | 318 | describe "#merge" do 319 | it 'merges given Hash' do 320 | merged = @deep_hash.merge(:no => "in between") 321 | merged.should == { :nested_1 => { :nested_2 => { :leaf_3 => "val3" }, :leaf_2 => ['arr'] }, :leaf_at_top => 'val1b', :no => 'in between' } 322 | end 323 | 324 | it 'returns a new instance' do 325 | merged = @deep_hash.merge(:no => "in between") 326 | merged.should_not equal(@deep_hash) 327 | end 328 | 329 | it 'returns instance of DeepHash' do 330 | merged = @deep_hash.merge(:no => "in between") 331 | merged.should be_an_instance_of(DeepHash) 332 | merged[:no].should == "in between" 333 | merged["no"].should == "in between" 334 | end 335 | 336 | it "converts all Hash values into DeepHashes" do 337 | merged = @deep_hash.merge({ :nested_1 => { 'nested_2' => { :leaf_3_also => "val3a" } }, :other1 => { "other2" => "other_val2" }}) 338 | merged[:nested_1].should be_an_instance_of(DeepHash) 339 | merged[:nested_1][:nested_2].should be_an_instance_of(DeepHash) 340 | merged[:other1].should be_an_instance_of(DeepHash) 341 | end 342 | 343 | it "converts string keys to symbol keys even if they occur deep in the given hash" do 344 | merged = @deep_hash.merge({ 'a' => { 'b' => { 'c' => { :d => :e }}}}) 345 | merged[:a].should == { :b => { :c => { :d => :e }}} 346 | merged[:a].should_not == { 'b' => { 'c' => { :d => :e }}} 347 | end 348 | 349 | it "DOES merge values where given hash has nil value" do 350 | merged = @deep_hash.merge(:a => { :b => nil }, :c => nil, :leaf_3_also => nil) 351 | merged[:a][:b].should be_nil 352 | merged[:c].should be_nil 353 | merged[:leaf_3_also].should be_nil 354 | end 355 | 356 | it "replaces child hashes, and does not merge them" do 357 | merged = @deep_hash.merge({ :nested_1 => { 'nested_2' => { :leaf_3_also => "val3a" } }, :other1 => { "other2" => "other_val2" }}) 358 | merged.should == { :nested_1 => { :nested_2 => { :leaf_3_also => "val3a" } }, :other1 => { :other2 => "other_val2" }, :leaf_at_top => 'val1b' } 359 | end 360 | end 361 | 362 | describe "#merge!" do 363 | it 'merges given Hash' do 364 | @deep_hash.merge!(:no => "in between") 365 | @deep_hash.should == { :nested_1 => { :nested_2 => { :leaf_3 => "val3" }, :leaf_2 => ['arr'] }, :leaf_at_top => 'val1b', :no => 'in between' } 366 | end 367 | 368 | it 'returns a new instance' do 369 | @deep_hash.merge!(:no => "in between") 370 | @deep_hash.should equal(@deep_hash) 371 | end 372 | 373 | it 'returns instance of DeepHash' do 374 | @deep_hash.merge!(:no => "in between") 375 | @deep_hash.should be_an_instance_of(DeepHash) 376 | @deep_hash[:no].should == "in between" 377 | @deep_hash["no"].should == "in between" 378 | end 379 | 380 | it "converts all Hash values into DeepHashes" do 381 | @deep_hash.merge!({ :nested_1 => { 'nested_2' => { :leaf_3_also => "val3a" } }, :other1 => { "other2" => "other_val2" }}) 382 | @deep_hash[:nested_1].should be_an_instance_of(DeepHash) 383 | @deep_hash[:nested_1][:nested_2].should be_an_instance_of(DeepHash) 384 | @deep_hash[:other1].should be_an_instance_of(DeepHash) 385 | end 386 | 387 | it "converts string keys to symbol keys even if they occur deep in the given hash" do 388 | @deep_hash.merge!({ 'a' => { 'b' => { 'c' => { :d => :e }}}}) 389 | @deep_hash[:a].should == { :b => { :c => { :d => :e }}} 390 | @deep_hash[:a].should_not == { 'b' => { 'c' => { :d => :e }}} 391 | end 392 | 393 | it "DOES merge values where given hash has nil value" do 394 | @deep_hash.merge!(:a => { :b => nil }, :c => nil, :leaf_3_also => nil) 395 | @deep_hash[:a][:b].should be_nil 396 | @deep_hash[:c].should be_nil 397 | @deep_hash[:leaf_3_also].should be_nil 398 | end 399 | 400 | it "replaces child hashes, and does not merge them" do 401 | @deep_hash = @deep_hash.merge!({ :nested_1 => { 'nested_2' => { :leaf_3_also => "val3a" } }, :other1 => { "other2" => "other_val2" }}) 402 | @deep_hash.should == { :nested_1 => { :nested_2 => { :leaf_3_also => "val3a" } }, :other1 => { :other2 => "other_val2" }, :leaf_at_top => 'val1b' } 403 | @deep_hash.should_not == { :nested_1 => { 'nested_2' => { :leaf_3_also => "val3a" } }, :other1 => { "other2" => "other_val2" }, :leaf_at_top => 'val1b' } 404 | end 405 | end 406 | 407 | describe "#reverse_merge" do 408 | it 'merges given Hash' do 409 | @deep_hash.reverse_merge!(:no => "in between", :leaf_at_top => 'NOT_USED') 410 | @deep_hash.should == { :nested_1 => { :nested_2 => { :leaf_3 => "val3" }, :leaf_2 => ['arr'] }, :leaf_at_top => 'val1b', :no => 'in between' } 411 | end 412 | 413 | it 'returns a new instance' do 414 | @deep_hash.reverse_merge!(:no => "in between") 415 | @deep_hash.should equal(@deep_hash) 416 | end 417 | 418 | it 'returns instance of DeepHash' do 419 | @deep_hash.reverse_merge!(:no => "in between") 420 | @deep_hash.should be_an_instance_of(DeepHash) 421 | @deep_hash[:no].should == "in between" 422 | @deep_hash["no"].should == "in between" 423 | end 424 | 425 | it "converts all Hash values into DeepHashes" do 426 | @deep_hash.reverse_merge!({ :nested_1 => { 'nested_2' => { :leaf_3_also => "val3a" } }, :other1 => { "other2" => "other_val2" }}) 427 | @deep_hash[:nested_1].should be_an_instance_of(DeepHash) 428 | @deep_hash[:nested_1][:nested_2].should be_an_instance_of(DeepHash) 429 | @deep_hash[:other1].should be_an_instance_of(DeepHash) 430 | end 431 | 432 | it "converts string keys to symbol keys even if they occur deep in the given hash" do 433 | merged = @deep_hash.reverse_merge({ 'a' => { 'b' => { 'c' => { :d => :e }}}}) 434 | merged[:a].should == { :b => { :c => { :d => :e }}} 435 | merged[:a].should_not == { 'b' => { 'c' => { :d => :e }}} 436 | end 437 | 438 | it "DOES merge values where given hash has nil value" do 439 | @deep_hash.reverse_merge!(:a => { :b => nil }, :c => nil) 440 | @deep_hash[:a][:b].should be_nil 441 | @deep_hash[:c].should be_nil 442 | end 443 | 444 | it "replaces child hashes, and does not merge them" do 445 | @deep_hash = @deep_hash.reverse_merge!({ :nested_1 => { 'nested_2' => { :leaf_3_also => "val3a" } }, :other1 => { "other2" => "other_val2" }}) 446 | @deep_hash.should == { :nested_1 => { :nested_2 => { :leaf_3 => "val3" }, :leaf_2 => ['arr'] }, :other1 => { :other2 => "other_val2" }, :leaf_at_top => 'val1b' } 447 | end 448 | end 449 | 450 | describe "#deep_merge!" do 451 | it "merges two subhashes when they share a key" do 452 | @deep_hash.deep_merge!(:nested_1 => { :nested_2 => { :leaf_3_also => "val3a" } }) 453 | @deep_hash.should == { :nested_1 => { :nested_2 => { :leaf_3_also => "val3a", :leaf_3 => "val3" }, :leaf_2 => ['arr'] }, :leaf_at_top => 'val1b' } 454 | end 455 | 456 | it "merges two subhashes when they share a symbolized key" do 457 | @deep_hash.deep_merge!(:nested_1 => { "nested_2" => { "leaf_3_also" => "val3a" } }) 458 | @deep_hash.should == { :nested_1 => { :nested_2 => { :leaf_3_also => "val3a", :leaf_3 => "val3" }, :leaf_2 => ['arr'] }, :leaf_at_top => "val1b" } 459 | end 460 | 461 | it "preserves values in the original" do 462 | @deep_hash.deep_merge! :other_key => "other_val" 463 | @deep_hash[:nested_1][:leaf_2].should == ['arr'] 464 | @deep_hash[:other_key].should == "other_val" 465 | end 466 | 467 | it "converts all Hash values into DeepHashes" do 468 | @deep_hash.deep_merge!({:nested_1 => { :nested_2 => { :leaf_3_also => "val3a" } }, :other1 => { "other2" => "other_val2" }}) 469 | @deep_hash[:nested_1].should be_an_instance_of(DeepHash) 470 | @deep_hash[:nested_1][:nested_2].should be_an_instance_of(DeepHash) 471 | @deep_hash[:other1].should be_an_instance_of(DeepHash) 472 | end 473 | 474 | it "converts string keys to symbol keys even if they occur deep in the given hash" do 475 | @deep_hash.deep_merge!({ 'a' => { 'b' => { 'c' => { :d => :e }}}}) 476 | @deep_hash[:a].should == { :b => { :c => { :d => :e }}} 477 | @deep_hash[:a].should_not == { 'b' => { 'c' => { :d => :e }}} 478 | end 479 | 480 | it "replaces values from the given hash" do 481 | @deep_hash.deep_merge!(:nested_1 => { :nested_2 => { :leaf_3 => "new_val3" }, :leaf_2 => { "other2" => "other_val2" }}) 482 | @deep_hash[:nested_1][:nested_2][:leaf_3].should == 'new_val3' 483 | @deep_hash[:nested_1][:leaf_2].should == { :other2 => "other_val2" } 484 | end 485 | 486 | it "replaces arrays and does not append to them" do 487 | @deep_hash.deep_merge!(:nested_1 => { :nested_2 => { :leaf_3 => [] }, :leaf_2 => ['val2'] }) 488 | @deep_hash[:nested_1][:nested_2][:leaf_3].should == [] 489 | @deep_hash[:nested_1][:leaf_2].should == ['val2'] 490 | end 491 | 492 | it "does not replaces values where given hash has nil value" do 493 | @deep_hash.deep_merge!(:nested_1 => { :leaf_2 => nil }, :leaf_at_top => '') 494 | @deep_hash[:nested_1][:leaf_2].should == ['arr'] 495 | @deep_hash[:leaf_at_top].should == "" 496 | end 497 | end 498 | 499 | 500 | describe "#deep_set" do 501 | it 'should set a new value (single arg)' do 502 | @deep_hash.deep_set :new_key, 'new_val' 503 | @deep_hash[:new_key].should == 'new_val' 504 | end 505 | it 'should set a new value (multiple args)' do 506 | @deep_hash.deep_set :nested_1, :nested_2, :new_key, 'new_val' 507 | @deep_hash[:nested_1][:nested_2][:new_key].should == 'new_val' 508 | end 509 | it 'should replace an existing value (single arg)' do 510 | @deep_hash.deep_set :leaf_at_top, 'new_val' 511 | @deep_hash[:leaf_at_top].should == 'new_val' 512 | end 513 | it 'should replace an existing value (multiple args)' do 514 | @deep_hash.deep_set :nested_1, :nested_2, 'new_val' 515 | @deep_hash[:nested_1][:nested_2].should == 'new_val' 516 | end 517 | it 'should auto-vivify intermediate hashes' do 518 | @deep_hash.deep_set :one, :two, :three, :four, 'new_val' 519 | @deep_hash[:one][:two][:three][:four].should == 'new_val' 520 | end 521 | end 522 | 523 | describe "#deep_delete" do 524 | it 'should remove the key from the array (multiple args)' do 525 | @deep_hash.deep_delete(:nested_1) 526 | @deep_hash[:nested_1].should be_nil 527 | @deep_hash.should == { :leaf_at_top => 'val1b'} 528 | end 529 | it 'should remove the key from the array (multiple args)' do 530 | @deep_hash.deep_delete(:nested_1, :nested_2, :leaf_3) 531 | @deep_hash[:nested_1][:nested_2][:leaf_3].should be_nil 532 | @deep_hash.should == {:leaf_at_top => "val1b", :nested_1 => {:leaf_2 => ['arr'], :nested_2 => {}}} 533 | end 534 | it 'should return the value if present (single args)' do 535 | returned_val = @deep_hash.deep_delete(:leaf_at_top) 536 | returned_val.should == 'val1b' 537 | end 538 | it 'should return the value if present (multiple args)' do 539 | returned_val = @deep_hash.deep_delete(:nested_1, :nested_2, :leaf_3) 540 | returned_val.should == 'val3' 541 | end 542 | it 'should return nil if the key is absent (single arg)' do 543 | returned_val = @deep_hash.deep_delete(:nested_1, :nested_2, :missing_key) 544 | returned_val.should be_nil 545 | end 546 | it 'should return nil if the key is absent (multiple args)' do 547 | returned_val = @deep_hash.deep_delete(:missing_key) 548 | returned_val.should be_nil 549 | end 550 | end 551 | 552 | end 553 | -------------------------------------------------------------------------------- /spec/configliere/define_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe "Configliere::Define" do 4 | before do 5 | @config = Configliere::Param.new :normal_param => 'normal' 6 | end 7 | 8 | describe 'defining any aspect of a param' do 9 | it 'adopts values' do 10 | @config.define :simple, :description => 'desc' 11 | @config.definition_of(:simple).should == { :description => 'desc'} 12 | end 13 | 14 | it 'returns self' do 15 | ret_val = @config.define :simple, :description => 'desc' 16 | ret_val.should equal(@config) 17 | end 18 | 19 | it 'merges new definitions' do 20 | @config.define :described_in_steps, :description => 'desc 1' 21 | @config.define :described_in_steps, :description => 'desc 2' 22 | @config.definition_of(:described_in_steps).should == { :description => 'desc 2'} 23 | @config.define :described_in_steps, :encrypted => true 24 | @config.definition_of(:described_in_steps).should == { :encrypted => true, :description => 'desc 2'} 25 | end 26 | 27 | it 'lists params defined as the given aspect' do 28 | @config.define :has_description, :description => 'desc 1' 29 | @config.define :also_has_description, :description => 'desc 2' 30 | @config.define :no_description, :something_else => 'foo' 31 | @config.params_with(:description).should include(:has_description) 32 | @config.params_with(:description).should include(:also_has_description) 33 | @config.params_with(:description).should_not include(:no_description) 34 | end 35 | end 36 | 37 | describe 'definition_of' do 38 | it 'with a param, gives me the description hash' do 39 | @config.define :has_description, :description => 'desc 1' 40 | @config.definition_of(:has_description).should == { :description => 'desc 1' } 41 | end 42 | it 'with a param and attr, gives me the description hash' do 43 | @config.define :has_description, :description => 'desc 1' 44 | @config.definition_of(:has_description, :description).should == 'desc 1' 45 | end 46 | it 'symbolizes the param' do 47 | @config.define :has_description, :description => 'desc 1' 48 | @config.definition_of('has_description').should == { :description => 'desc 1' } 49 | @config.definition_of('has_description', 'description').should be_nil 50 | end 51 | end 52 | 53 | describe 'has_definition?' do 54 | before do 55 | @config.define :i_am_defined, :description => 'desc 1' 56 | end 57 | it 'is true if defined (one arg)' do 58 | @config.has_definition?(:i_am_defined).should == true 59 | end 60 | it 'is false if not defined (one arg)' do 61 | @config.has_definition?(:i_am_not_defined).should == false 62 | end 63 | it 'is true if defined and attribute is defined' do 64 | @config.has_definition?(:i_am_defined, :description).should == true 65 | end 66 | it 'is false if defined and attribute is defined' do 67 | @config.has_definition?(:i_am_defined, :zoink).should == false 68 | end 69 | it 'is false if not defined and attribute is given' do 70 | @config.has_definition?(:i_am_not_defined, :zoink).should == false 71 | end 72 | end 73 | 74 | it 'takes a description' do 75 | @config.define :has_description, :description => 'desc 1' 76 | @config.define :also_has_description, :description => 'desc 2' 77 | @config.definition_of(:has_description, :description).should == 'desc 1' 78 | end 79 | 80 | describe 'type coercion' do 81 | [ 82 | [:boolean, '0', false], [:boolean, 0, false], [:boolean, '', false], [:boolean, [], true], [:boolean, nil, nil], 83 | [:boolean, '1', true], [:boolean, 1, true], [:boolean, '5', true], [:boolean, 'true', true], 84 | [Integer, '5', 5], [Integer, 5, 5], [Integer, nil, nil], [Integer, '', nil], 85 | [Integer, '5', 5], [Integer, 5, 5], [Integer, nil, nil], [Integer, '', nil], 86 | [Float, '5.2', 5.2], [Float, 5.2, 5.2], [Float, nil, nil], [Float, '', nil], 87 | [Symbol, 'foo', :foo], [Symbol, :foo, :foo], [Symbol, nil, nil], [Symbol, '', nil], 88 | [Date, '1985-11-05', Date.parse('1985-11-05')], [Date, nil, nil], [Date, '', nil], [Date, 'blah', nil], 89 | [DateTime, '1985-11-05 11:00:00', DateTime.parse('1985-11-05 11:00:00')], [DateTime, nil, nil], [DateTime, '', nil], [DateTime, 'blah', nil], 90 | [Array, ['this', 'that', 'thother'], ['this', 'that', 'thother']], 91 | [Array, 'this,that,thother', ['this', 'that', 'thother']], 92 | [Array, 'alone', ['alone'] ], 93 | [Array, '', [] ], 94 | [Array, nil, nil ], 95 | ].each do |type, orig, desired| 96 | it "for #{type} converts #{orig.inspect} to #{desired.inspect}" do 97 | @config.define :has_type, :type => type 98 | @config[:has_type] = orig ; @config.resolve! ; @config[:has_type].should == desired 99 | end 100 | end 101 | 102 | it 'raises an error (FIXME: on resolve, which is not that great) if you define an unknown type' do 103 | @config.define :has_type, :type => 'bogus, man' 104 | @config[:has_type] = "WHOA" ; 105 | expect{ @config.resolve! }.to raise_error(ArgumentError, /Unknown type.*bogus, man/) 106 | end 107 | 108 | it 'converts :now to the current moment' do 109 | @config.define :has_type, :type => DateTime 110 | @config[:has_type] = 'now' ; @config.resolve! ; @config[:has_type].should be_within(4).of(DateTime.now) 111 | @config[:has_type] = :now ; @config.resolve! ; @config[:has_type].should be_within(4).of(DateTime.now) 112 | @config.define :has_type, :type => Date 113 | @config[:has_type] = :now ; @config.resolve! ; @config[:has_type].should be_within(4).of(Date.today) 114 | @config[:has_type] = 'now' ; @config.resolve! ; @config[:has_type].should be_within(4).of(Date.today) 115 | end 116 | end 117 | 118 | describe 'creates magical methods' do 119 | before do 120 | @config.define :has_magic_method, :default => 'val1' 121 | @config[:no_magic_method] = 'val2' 122 | end 123 | it 'answers to a getter if the param is defined' do 124 | @config.has_magic_method.should == 'val1' 125 | end 126 | it 'answers to a setter if the param is defined' do 127 | @config.has_magic_method = 'new_val1' 128 | @config.has_magic_method.should == 'new_val1' 129 | @config[:has_magic_method].should == 'new_val1' 130 | end 131 | it 'does not answer to a getter if the param is not defined' do 132 | lambda{ @config.no_magic_method }.should raise_error(NoMethodError) 133 | end 134 | it 'does not answer to a setter if the param is not defined' do 135 | lambda{ @config.no_magic_method = 3 }.should raise_error(NoMethodError) 136 | end 137 | end 138 | 139 | describe 'defining requireds' do 140 | before do 141 | @config.define :param_1, :required => true 142 | @config.define :param_2, :required => true 143 | @config.define :optional_1, :required => false 144 | @config.define :optional_2 145 | end 146 | it 'lists required params' do 147 | @config.params_with(:required).should include(:param_1) 148 | @config.params_with(:required).should include(:param_2) 149 | end 150 | it 'counts false values as present' do 151 | @config.defaults :param_1 => true, :param_2 => false 152 | @config.validate!.should equal(@config) 153 | end 154 | it 'counts nil-but-set values as missing' do 155 | @config.defaults :param_1 => true, :param_2 => nil 156 | lambda{ @config.validate! }.should raise_error(RuntimeError) 157 | end 158 | it 'counts never-set values as missing' do 159 | lambda{ @config.validate! }.should raise_error(RuntimeError) 160 | end 161 | it 'lists all missing values when it raises' do 162 | lambda{ @config.validate! }.should raise_error(RuntimeError, "Missing values for: param_1, param_2") 163 | end 164 | end 165 | 166 | describe 'defining deep keys' do 167 | it 'allows required params' do 168 | @config.define 'delorean.power_supply', :required => true 169 | @config[:'delorean.power_supply'] = 'household waste' 170 | @config.params_with(:required).should include(:'delorean.power_supply') 171 | @config.should == { :normal_param=>"normal", :delorean => { :power_supply => 'household waste' } } 172 | lambda{ @config.validate! }.should_not raise_error 173 | end 174 | 175 | it 'allows flags' do 176 | @config.define 'delorean.power_supply', :flag => 'p' 177 | @config.use :commandline 178 | ARGV.replace ['-p', 'household waste'] 179 | @config.params_with(:flag).should include(:'delorean.power_supply') 180 | @config.resolve! 181 | @config.should == { :normal_param=>"normal", :delorean => { :power_supply => 'household waste' } } 182 | ARGV.replace [] 183 | end 184 | 185 | it 'type converts' do 186 | @config.define 'delorean.power_supply', :type => Array 187 | @config.use :commandline 188 | ARGV.replace ['--delorean.power_supply=household waste,plutonium,lightning'] 189 | @config.definition_of('delorean.power_supply', :type).should == Array 190 | @config.resolve! 191 | @config.should == { :normal_param=>"normal", :delorean => { :power_supply => ['household waste', 'plutonium', 'lightning'] } } 192 | ARGV.replace [] 193 | end 194 | end 195 | 196 | describe '#resolve!' do 197 | it 'calls super and returns self' do 198 | Configliere::ParamParent.class_eval do def resolve!() dummy ; end ; end 199 | @config.should_receive(:dummy) 200 | @config.resolve!.should equal(@config) 201 | Configliere::ParamParent.class_eval do def resolve!() self ; end ; end 202 | end 203 | end 204 | 205 | describe '#validate!' do 206 | it 'calls super and returns self' do 207 | Configliere::ParamParent.class_eval do def validate!() dummy ; end ; end 208 | @config.should_receive(:dummy) 209 | @config.validate!.should equal(@config) 210 | Configliere::ParamParent.class_eval do def validate!() self ; end ; end 211 | end 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /spec/configliere/encrypted_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | module Configliere ; module Crypter ; CIPHER_TYPE = 'aes-128-cbc' ; end ; end 4 | 5 | describe "Configliere::Encrypted", :if => check_openssl do 6 | require 'configliere/crypter' 7 | 8 | before do 9 | @config = Configliere::Param.new :secret => 'encrypt_me', :normal_param => 'normal' 10 | @config.use :encrypted 11 | @config.define :secret, :encrypted => true 12 | @config[:encrypt_pass] = 'pass' 13 | end 14 | 15 | if Configliere::Crypter::CIPHER_TYPE == 'aes-256-cbc' 16 | let(:encrypted_str){ "KohCTcXr1aAulopntmZ8f5Gqa7PzsBmz+R2vFGYrAeg=\n" } 17 | let(:encrypted_foo_val){ "cc+Bp5jMUBHFCvPNZIfleeatB4IGaaXjVINl12HOpcs=\n" } 18 | elsif Configliere::Crypter::CIPHER_TYPE == 'aes-128-cbc' 19 | let(:encrypted_str){ "mHse6HRTANh8JpIfIuyANQ8b2rXAf0+/3pzQnYsd8LE=\n" } 20 | let(:encrypted_foo_val){ "cc+Bp5jMUBHFCvPNZIfleZYRoDmLK1LSxPkAMemhDTQ=\n" } 21 | else 22 | warn "Can't make test strings for #{Configliere::Crypter::CIPHER_TYPE} cipher" 23 | end 24 | let(:foo_val_iv){ Base64.decode64(encrypted_foo_val)[0..15] } 25 | 26 | 27 | describe "Crypter" do 28 | it "encrypts" do 29 | # Force the same initialization vector as used to prepare the test value 30 | @cipher = Configliere::Crypter.send(:new_cipher, :encrypt, 'sekrit') 31 | Configliere::Crypter.should_receive(:new_cipher).and_return(@cipher) 32 | @cipher.should_receive(:random_iv).and_return foo_val_iv 33 | # OK so do the test now. 34 | Configliere::Crypter.encrypt('foo_val', 'sekrit').should == encrypted_foo_val 35 | end 36 | 37 | it "decrypts" do 38 | Configliere::Crypter.decrypt(encrypted_foo_val, 'sekrit').should == 'foo_val' 39 | end 40 | end 41 | 42 | 43 | describe 'defines encrypted params' do 44 | it 'with :encrypted => true' do 45 | @config.send(:encrypted_params).should include(:secret) 46 | end 47 | it 'but not if :encrypted => false' do 48 | @config.define :another_param, :encrypted => false 49 | @config.send(:encrypted_params).should_not include(:another_param) 50 | @config.send(:encrypted_params).should include(:secret) 51 | end 52 | it 'only if :encrypted is given' do 53 | @config.send(:encrypted_params).should_not include(:missing_param) 54 | end 55 | end 56 | 57 | describe 'the encrypt_pass' do 58 | it 'will take an environment variable if any exists' do 59 | @config[:encrypt_pass] = nil 60 | ENV.should_receive(:[]).with('ENCRYPT_PASS').at_least(:once).and_return('monkey') 61 | @config.send(:export) 62 | @config.send(:instance_variable_get, "@encrypt_pass").should == 'monkey' 63 | end 64 | it 'will take an internal value if given, and remove it' do 65 | @config[:encrypt_pass] = 'hello' 66 | @config.send(:export) 67 | @config.send(:instance_variable_get, "@encrypt_pass").should == 'hello' 68 | @config[:encrypt_pass].should be_nil 69 | @config.has_key?(:encrypt_pass).should_not be_true 70 | end 71 | end 72 | 73 | describe 'encrypts' do 74 | it 'all params with :encrypted' do 75 | Configliere::Crypter.should_receive(:encrypt).with('encrypt_me', 'pass').and_return('ok_encrypted') 76 | @config.send(:export).should == { :normal_param => 'normal', :encrypted_secret => 'ok_encrypted'} 77 | end 78 | 79 | it 'fails unless encrypt_pass is set' do 80 | # create the config but don't set an encrypt_pass 81 | @config = Configliere::Param.new :secret => 'encrypt_me', :normal_param => 'normal' 82 | @config.use :encrypted 83 | lambda{ @config.send(:encrypted, @config[:secret]) }.should raise_error('Missing encryption password!') 84 | end 85 | end 86 | 87 | describe 'decrypts' do 88 | it 'all params marked encrypted' do 89 | @config.delete :secret 90 | @config.defaults :encrypted_secret => 'decrypt_me' 91 | Configliere::Crypter.should_receive(:decrypt).with('decrypt_me', 'pass').and_return('ok_decrypted') 92 | @config.send(:resolve_encrypted!) 93 | @config.should == { :normal_param => 'normal', :secret => 'ok_decrypted' } 94 | end 95 | end 96 | 97 | describe 'loading a file' do 98 | it 'encrypts' do 99 | Configliere::Crypter.should_receive(:encrypt).and_return(encrypted_str) 100 | FileUtils.stub(:mkdir_p) 101 | File.should_receive(:open).and_yield([]) 102 | YAML.should_receive(:dump).with({ :normal_param => "normal", :encrypted_secret => encrypted_str }) 103 | @config.save! '/fake/file' 104 | end 105 | it 'decrypts' do 106 | # encrypted_str = Configliere::Crypter.encrypt('decrypt_me', 'pass') 107 | @hsh = { :loaded_param => "loaded", :encrypted_secret => encrypted_str } 108 | File.stub(:open) 109 | YAML.should_receive(:load).and_return(@hsh) 110 | @config.read 'file.yaml' 111 | @config.resolve! 112 | @config.should_not include(:encrypted_secret) 113 | @config.should == { :loaded_param => "loaded", :secret => 'decrypt_me', :normal_param => 'normal' } 114 | end 115 | end 116 | 117 | describe '#resolve!' do 118 | it 'calls super and returns self' do 119 | Configliere::ParamParent.class_eval do def resolve!() dummy ; end ; end 120 | @config.should_receive(:dummy) 121 | @config.resolve!.should equal(@config) 122 | Configliere::ParamParent.class_eval do def resolve!() self ; end ; end 123 | end 124 | 125 | it 'removes the encrypt_pass from sight' do 126 | @config[:encrypt_pass] = 'hello' 127 | @config.resolve! 128 | @config.send(:instance_variable_get, "@encrypt_pass").should == 'hello' 129 | @config[:encrypt_pass].should be_nil 130 | @config.has_key?(:encrypt_pass).should_not be_true 131 | end 132 | end 133 | 134 | describe '#validate!' do 135 | it 'calls super and returns self' do 136 | Configliere::ParamParent.class_eval do def validate!() dummy ; end ; end 137 | @config.should_receive(:dummy) 138 | @config.validate!.should equal(@config) 139 | Configliere::ParamParent.class_eval do def validate!() self ; end ; end 140 | end 141 | end 142 | 143 | end 144 | -------------------------------------------------------------------------------- /spec/configliere/env_var_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe "Configliere::EnvVar" do 4 | before do 5 | @config = Configliere::Param.new 6 | @config.use :env_var 7 | end 8 | 9 | describe 'environment variables can be defined' do 10 | it 'with #env_vars, a simple value like "HOME" uses the corresponding key :home' do 11 | ENV.should_receive(:[]).with('HOME').and_return('/fake/path') 12 | @config.env_vars 'HOME' 13 | @config[:home].should == '/fake/path' 14 | end 15 | 16 | it 'with #env_vars, a hash pairs environment variables into the individual params' do 17 | ENV.should_receive(:[]).with('HOME').and_return('/fake/path') 18 | ENV.should_receive(:[]).with('POWER_SUPPLY').and_return('1.21 jigawatts') 19 | @config.env_vars :home => 'HOME', 'delorean.power_supply' => 'POWER_SUPPLY' 20 | @config[:home].should == '/fake/path' 21 | @config[:delorean][:power_supply].should == '1.21 jigawatts' 22 | end 23 | 24 | it 'with #define' do 25 | ENV.should_receive(:[]).with('HOME').and_return('/fake/path') 26 | ENV.should_receive(:[]).with('POWER_SUPPLY').and_return('1.21 jigawatts') 27 | @config.define :home, :env_var => 'HOME' 28 | @config.define 'delorean.power_supply', :env_var => 'POWER_SUPPLY' 29 | @config[:home].should == '/fake/path' 30 | @config[:delorean][:power_supply].should == '1.21 jigawatts' 31 | end 32 | end 33 | 34 | describe '#resolve!' do 35 | it 'calls super and returns self' do 36 | Configliere::ParamParent.class_eval do def resolve!() dummy ; end ; end 37 | @config.should_receive(:dummy) 38 | @config.resolve!.should equal(@config) 39 | Configliere::ParamParent.class_eval do def resolve!() self ; end ; end 40 | end 41 | end 42 | 43 | describe '#validate!' do 44 | it 'calls super and returns self' do 45 | Configliere::ParamParent.class_eval do def validate!() dummy ; end ; end 46 | @config.should_receive(:dummy) 47 | @config.validate!.should equal(@config) 48 | Configliere::ParamParent.class_eval do def validate!() self ; end ; end 49 | end 50 | end 51 | 52 | end 53 | 54 | -------------------------------------------------------------------------------- /spec/configliere/param_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe "Configliere::Param" do 4 | before do 5 | @config = Configliere::Param.new :hat => :cat, :basket => :lotion, :moon => { :man => :smiling } 6 | end 7 | 8 | describe 'calling #defaults' do 9 | it 'deep_merges new params' do 10 | @config.defaults :basket => :tasket, :moon => { :cow => :jumping } 11 | @config.should == { :hat => :cat, :basket => :tasket, :moon => { :man => :smiling, :cow => :jumping } } 12 | end 13 | it 'returns self, to allow chaining' do 14 | obj = @config.defaults(:basket => :ball) 15 | obj.should equal(@config) 16 | end 17 | end 18 | 19 | describe 'adding plugins with #use' do 20 | before do 21 | Configliere.should_receive(:use).with(:foobar) 22 | end 23 | it 'requires the corresponding library' do 24 | obj = @config.use(:foobar) 25 | end 26 | it 'returns self, to allow chaining' do 27 | obj = @config.use(:foobar) 28 | obj.should equal(@config) 29 | end 30 | it 'invokes the on_use handler' do 31 | Configliere::Param.on_use(:foobar) do 32 | method_on_config(:param) 33 | end 34 | @config.should_receive(:method_on_config).with(:param) 35 | @config.use(:foobar) 36 | end 37 | end 38 | 39 | describe '#resolve!' do 40 | it 'calls super and returns self' do 41 | Configliere::ParamParent.class_eval do def resolve!() dummy ; end ; end 42 | @config.should_receive(:dummy) 43 | @config.resolve!.should equal(@config) 44 | Configliere::ParamParent.class_eval do def resolve!() self ; end ; end 45 | end 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /spec/configliere/prompt_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | # Highline does not work with JRuby 1.7.0+ as of Mid 2012. See https://github.com/JEG2/highline/issues/41. 4 | 5 | describe "Configliere::Prompt", :if => load_sketchy_lib('highline/import') do 6 | before do 7 | @config = Configliere::Param.new 8 | @config.use :prompt 9 | @config.define :underpants, :description => 'boxers or briefs' 10 | end 11 | 12 | describe 'when the value is already set, #prompt_for' do 13 | it 'returns the value' do 14 | @config[:underpants] = :boxers 15 | @config.prompt_for(:underpants).should == :boxers 16 | end 17 | it 'returns the value even if nil' do 18 | @config[:underpants] = nil 19 | @config.prompt_for(:underpants).should == nil 20 | end 21 | it 'returns the value even if nil' do 22 | @config[:underpants] = false 23 | @config.prompt_for(:underpants).should == false 24 | end 25 | end 26 | 27 | describe 'when prompting, #prompt_for' do 28 | it 'prompts for a value if missing' do 29 | @config.should_receive(:ask).with("surprise_param? ") 30 | @config.prompt_for(:surprise_param) 31 | end 32 | it 'uses an explicit hint' do 33 | @config.should_receive(:ask).with("underpants (wearing any)? ") 34 | @config.prompt_for(:underpants, "wearing any") 35 | end 36 | it 'uses the description as hint if none given' do 37 | @config.should_receive(:ask).with("underpants (boxers or briefs)? ") 38 | @config.prompt_for(:underpants) 39 | end 40 | end 41 | 42 | describe '#resolve!' do 43 | it 'calls super and returns self' do 44 | Configliere::ParamParent.class_eval do def resolve!() dummy ; end ; end 45 | @config.should_receive(:dummy) 46 | @config.resolve!.should equal(@config) 47 | Configliere::ParamParent.class_eval do def resolve!() self ; end ; end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/configliere_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('spec_helper', File.dirname(__FILE__)) 2 | 3 | describe "Configliere" do 4 | it 'creates a global variable Settings, for universality' do 5 | Settings.class.should == Configliere::Param 6 | end 7 | it 'creates a global method Settings, so you can say Settings(:foo => :bar)' do 8 | Settings.should_receive(:defaults).with(:foo => :bar) 9 | Settings(:foo => :bar) 10 | end 11 | 12 | it 'requires corresponding plugins when you call use' do 13 | lambda{ Configliere.use(:param, :foo) }.should raise_error(LoadError, /no.*load.*configliere\/foo/) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' ; Bundler.require(:default, :development, :test) 2 | require 'rspec/autorun' 3 | require 'yaml' 4 | 5 | puts "Running specs in version #{RUBY_VERSION} on #{RUBY_PLATFORM} #{RUBY_DESCRIPTION}" 6 | 7 | if ENV['CONFIGLIERE_COV'] 8 | require 'simplecov' 9 | SimpleCov.start 10 | end 11 | 12 | RSpec.configure do |config| 13 | config.treat_symbols_as_metadata_keys_with_true_values = true 14 | 15 | def load_sketchy_lib(lib) 16 | begin 17 | require lib 18 | yield if block_given? 19 | return true 20 | rescue LoadError, StandardError => err 21 | warn "#{RUBY_DESCRIPTION} doesn't seem to like #{lib}: got error" 22 | warn " #{err.class} #{err}" 23 | warn "Skipping specs on '#{caller(2).first}'" 24 | return false 25 | end 26 | end 27 | 28 | def capture_help_message 29 | stderr_output = '' 30 | subject.should_receive(:warn){|str| stderr_output << str } 31 | begin 32 | yield 33 | fail('should exit via system exit') 34 | rescue SystemExit 35 | true # pass 36 | end 37 | stderr_output 38 | end 39 | 40 | def check_openssl 41 | load_sketchy_lib('openssl') do 42 | cipher = OpenSSL::Cipher::Cipher.new('aes-128-cbc') 43 | cipher.encrypt 44 | cipher.key = Digest::SHA256.digest("HI JRUBY") 45 | cipher.iv = iv = cipher.random_iv 46 | ciphertext = cipher.update("O HAI TO YOU!") 47 | ciphertext << cipher.final 48 | # p [__LINE__, '128-bit encryption is OK', ciphertext] 49 | # cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc') 50 | # cipher.encrypt 51 | # cipher.key = Digest::SHA256.digest("HI JRUBY") 52 | # cipher.iv = iv = cipher.random_iv 53 | # ciphertext = cipher.update("O HAI TO YOU!") 54 | # ciphertext << cipher.final 55 | # p [__LINE__, '256-bit encryption is OK', ciphertext] 56 | end 57 | end 58 | 59 | end 60 | 61 | require 'configliere' 62 | -------------------------------------------------------------------------------- /src/main/java/com/infochimps/config/Configliere.java: -------------------------------------------------------------------------------- 1 | package com.infochimps.config; 2 | 3 | import com.infochimps.util.HttpHelper; 4 | import com.infochimps.vayacondios.ItemSets; 5 | import com.infochimps.vayacondios.StandardVCDLink; 6 | import com.infochimps.vayacondios.VayacondiosClient; 7 | 8 | import static com.infochimps.util.CurrentClass.getLogger; 9 | import static com.infochimps.vayacondios.ItemSets.Item; 10 | 11 | import java.io.FileNotFoundException; 12 | import java.io.FileReader; 13 | import java.io.IOException; 14 | import java.util.List; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | import java.util.Iterator; 19 | 20 | import org.slf4j.Logger; 21 | 22 | public class Configliere { 23 | public static void loadFlatItemSet(String topic, String id) { 24 | loadFlatItemSet(propertyOrDie("vayacondios.organization"), topic, id); 25 | } 26 | 27 | public static void loadFlatItemSet(String orgName, String topic, String id) { 28 | loadFlatItemSet(orgName, topic, id, Boolean.FALSE); 29 | } 30 | 31 | public static void loadFlatItemSet(String orgName, String topic, String id, Boolean withOrg) { 32 | ItemSets org = organizations.get(orgName); 33 | StandardVCDLink 34 | .forceLegacy(Boolean.valueOf(propertyOr("vayacondios.legacy", "false"))); 35 | if (org == null) { 36 | org = (new VayacondiosClient( 37 | propertyOrDie("vayacondios.host"), 38 | Integer.parseInt(propertyOrDie("vayacondios.port"))). 39 | organization(orgName). 40 | itemsets()); 41 | 42 | organizations.put(orgName, org); 43 | } 44 | 45 | StringBuilder builder = new StringBuilder(); 46 | 47 | List items = null; 48 | try { items = org.fetch(topic, id); } 49 | catch (IOException ex) { 50 | LOG.warn("error loading " + topic + "." + id + " from vayacondios:", ex); 51 | return; 52 | } 53 | 54 | String propertyName = topic + "." + id; 55 | if (withOrg) { 56 | propertyName = orgName + "." + propertyName; 57 | } 58 | 59 | if (items.size() == 0) System.setProperty(propertyName, ""); 60 | else { 61 | Iterator iter = items.iterator(); 62 | builder.append(iter.next().getObject().toString()); 63 | while (iter.hasNext()) 64 | builder.append(JOIN).append(iter.next().getObject().toString()); 65 | 66 | System.setProperty(propertyName, builder.toString()); 67 | } 68 | } 69 | 70 | public static void loadConfigFileOrDie(String name) { 71 | try { 72 | System.err.println("loading config from: " + name); 73 | System.getProperties().load(new FileReader(name)); 74 | } catch (FileNotFoundException ex) { 75 | throw new AssertionError(ex); 76 | } catch (IOException ex) { 77 | throw new AssertionError(ex); 78 | } 79 | } 80 | 81 | public static String propertyOr(String name, String alternative) { 82 | String property = System.getProperty(name); 83 | return (property == null) ? alternative : property; 84 | } 85 | 86 | public static String propertyOrDie(String name) { 87 | String property = System.getProperty(name); 88 | // Java assertions are disabled by default, so do this instead. 89 | if (property == null) 90 | throw new AssertionError("property " + name + " not provided"); 91 | return property; 92 | } 93 | 94 | private static final String JOIN = ","; 95 | private static Map organizations = 96 | new HashMap(); 97 | private static final Logger LOG = getLogger(); 98 | } -------------------------------------------------------------------------------- /src/main/java/com/infochimps/config/IntegrationTests.java: -------------------------------------------------------------------------------- 1 | package com.infochimps.config; 2 | 3 | import com.infochimps.config.Configliere; 4 | import com.infochimps.vayacondios.StandardVCDLink; 5 | import static com.infochimps.config.Configliere.propertyOr; 6 | import static com.infochimps.config.Configliere.propertyOrDie; 7 | import static com.infochimps.vayacondios.ItemSets.ItemSet; 8 | import static com.infochimps.vayacondios.ItemSets.Item; 9 | import com.infochimps.vayacondios.VayacondiosClient; 10 | 11 | import java.io.*; 12 | import java.util.*; 13 | 14 | public class IntegrationTests { 15 | final static String VCD_HOST = "localhost"; 16 | final static int VCD_PORT = 8000; 17 | final static String VCD_TOPIC = "configliere"; 18 | final static String VCD_ID = "samples"; 19 | 20 | public static void main(String argv[]) throws IOException { 21 | StandardVCDLink 22 | .forceLegacy(Boolean.valueOf(propertyOr("vayacondios.legacy", "false"))); 23 | VayacondiosClient client = new VayacondiosClient(VCD_HOST, VCD_PORT); 24 | 25 | System.setProperty("vayacondios.host", VCD_HOST); 26 | System.setProperty("vayacondios.port", Integer.toString(VCD_PORT)); 27 | 28 | String result; 29 | 30 | result = populateSet(client, "test_one", "foo", "bar", "baz"); 31 | System.setProperty("vayacondios.organization", "test_one"); 32 | Configliere.loadFlatItemSet(VCD_TOPIC, VCD_ID); 33 | testProperty(result); 34 | 35 | result = populateSet(client, "test_two", "bif", "bam", "buz"); 36 | Configliere.loadFlatItemSet("test_two", VCD_TOPIC, VCD_ID); 37 | testProperty(result); 38 | 39 | depopulateSet(client, "test_one"); 40 | depopulateSet(client, "test_two"); 41 | } 42 | 43 | public static void depopulateSet(VayacondiosClient client, String orgName) 44 | throws IOException { 45 | client.organization(orgName).itemsets(). 46 | create(VCD_TOPIC, VCD_ID, Collections.EMPTY_LIST); 47 | } 48 | 49 | public static String populateSet(VayacondiosClient client, 50 | String orgName, 51 | String... itemStrings) throws IOException { 52 | ArrayList items = new ArrayList(); 53 | for (String str : itemStrings) items.add(new Item(str)); 54 | client.organization(orgName).itemsets().create(VCD_TOPIC, VCD_ID, items); 55 | StringBuilder builder = new StringBuilder(); 56 | builder.append(itemStrings[0]); 57 | for (int i = 1; i < itemStrings.length; i++) { 58 | builder.append(","); 59 | builder.append(itemStrings[i]); 60 | } 61 | return builder.toString(); 62 | } 63 | 64 | public static void testProperty(String expected) { 65 | String propName = VCD_TOPIC + "." + VCD_ID; 66 | String result = System.getProperty(propName); 67 | 68 | if (result == null) 69 | System.out.println("\033[31mFAIL\033[0m: system property not populated"); 70 | else if (!result.equals(expected)) { 71 | System.out.println("\033[31mFAIL\033[0m: result not correct."); 72 | System.out.println(" expected " + expected + " but saw"); 73 | System.out.println(" " + result); 74 | } else { 75 | System.out.println("\033[32mSUCCESS\033[0m saw all expected items"); 76 | } 77 | 78 | } 79 | } 80 | --------------------------------------------------------------------------------