toggle 0.0.0.alpha → 1.0.0.rc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
+ .rvmrc
6
7
  Gemfile.lock
7
8
  InstalledFiles
8
9
  _yardoc
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,14 @@
1
+ Contributing
2
+ ============
3
+
4
+ If you would like to contribute code to Toggle you can do so through GitHub by
5
+ forking the repository and sending a pull request.
6
+
7
+ When submitting code, please make every effort to follow existing conventions
8
+ and style in order to keep the code as readable as possible.
9
+
10
+ Before your code can be accepted into the project you must also sign the
11
+ [Individual Contributor License Agreement (CLA)][1].
12
+
13
+
14
+ [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in toggle.gemspec
3
+ # Specify your gem's dependencies in switch.gemspec
4
4
  gemspec
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2013 Square Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ 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
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md CHANGED
@@ -1,6 +1,37 @@
1
1
  # Toggle
2
2
 
3
- Toggle is coming soon!
3
+ Toggle provides an organized and flexible framework to set up, manage, and
4
+ switch between different configuration settings in your Ruby scripts.
5
+
6
+ ## Why?
7
+
8
+ Ensuring that a script has the correct configuration settings can become a real
9
+ headache. Have you ever had to run a script under different environment
10
+ specifications or had to share a script that requires different settings based
11
+ on who is running the code?
12
+
13
+ You may have resorted to storing configuration information in a hash to the top
14
+ of a given script to provide some flexibility. This can work for a script or
15
+ two and when your on a small team, but as you write more code or increase your
16
+ team's size the need for organization while still maintaining flexibility
17
+ quickly arises.
18
+
19
+ Having a common pattern around how per-project configurations are handled
20
+ becomes a big plus. Projects like [rbenv-vars](https://github.com/sstephenson/rbenv-vars)
21
+ came about to help solve issues like these.
22
+
23
+ Toggle provides a project with rbenv-vars-like functionality with two main
24
+ additions:
25
+
26
+ 1. rbenv is not required
27
+ 2. you can specify *multiple* environment setups instead of just one on a
28
+ per-project basis, each of which is easily switchable to either programmatically
29
+ or at runtime.
30
+
31
+ Additionally, Toggle provides a command line interface to facilitate setting up
32
+ this framework along with a set of options to quickly inspect which variables are
33
+ available within a project and what each variable is set to for a given environment
34
+ specification.
4
35
 
5
36
  ## Installation
6
37
 
@@ -16,14 +47,279 @@ Or install it yourself as:
16
47
 
17
48
  $ gem install toggle
18
49
 
19
- ## Usage
50
+ ## Basic Usage
51
+
52
+ To start using Toggle you first need a configuration file. This file
53
+ will typically be in YAML format and can contain inline ERB. The file's content
54
+ will include all the different configuration sections you want to be able to toggle to.
55
+ Each section should be namespaced appropriately.
56
+
57
+ As an example let's say we have two different script configurations we'd like to
58
+ have available and we name each `alpha` and `beta` respectively. Then our
59
+ configuration file might look like:
60
+
61
+ ```yaml
62
+ # Sample config.yml demonstrating Toggle usage.
63
+ # Notice that each section contains the same keys but the values vary.
64
+ :alpha:
65
+ :name: mr_alpha
66
+ :secret: <%= ENV['ALPHA_SECRET'] %> # pretend this is "alpha-secret"
67
+
68
+ :beta:
69
+ :name: mr_beta
70
+ :secret: <%= ENV['BETA_SECRET'] %> # pretend this is "beta-secret"
71
+ ```
72
+
73
+ Now in any script we can leverage Toggle to toggle between each configuration
74
+ section by setting the `key` attribute, which will load the corresponding
75
+ configuration section:
76
+
77
+ ```ruby
78
+ require 'toggle'
79
+ toggle = Toggle.new config_filepath: './config.yml'
80
+
81
+ toggle.key = :alpha
82
+ puts "#{toggle[:name]} has a secret: #{toggle[:secret]}!"
83
+ #=> mr_alpha has a secret: alpha-secret
84
+
85
+ toggle.key = :beta
86
+ puts "#{toggle[:name]} has a secret: #{toggle[:secret]}!"
87
+ #=> mr_beta has a secret: beta-secret
88
+ ```
89
+
90
+ Toggle also supports temporary access to a configuration section by passing a
91
+ block to `#using`:
92
+
93
+ ```ruby
94
+ require 'toggle'
95
+
96
+ toggle = Toggle.new config_filepath: './config.yml',
97
+ key: :beta
98
+
99
+ toggle.using(:alpha) do |s|
100
+ puts "#{s[:name]} has a secret: #{s[:secret]}!"
101
+ #=> mr_alpha has a secret: alpha-secret
102
+ end
103
+
104
+ puts "#{toggle[:name]} has a secret: #{toggle[:secret]}!"
105
+ #=> mr_beta has a secret: beta-secret
106
+ ```
107
+
108
+ As an alternative to specifying the `key` attribute programmatically, you can
109
+ create a key file:
110
+
111
+ ```yaml
112
+ # sample key.yml file
113
+ alpha
114
+ ```
115
+
116
+ and then set Toggle's `key_filepath` attribute to specify where the `key`'s
117
+ value should be derived from:
118
+
119
+ ```ruby
120
+ require 'toggle'
121
+
122
+ toggle = Toggle.new config_filepath: './config.yml',
123
+ key_filepath: './key.yml'
124
+
125
+ puts "#{toggle[:name]} has a secret: #{toggle[:secret]}!"
126
+ #=> mr_alpha has a secret: alpha-secret
127
+ ```
128
+
129
+ ## Realworld Use Case: Runtime Toggling
130
+
131
+ Let's say there is a developer named Jane and she wants to author a script that
132
+ connects to a database server, pulls in data and does some processing, and then
133
+ emails the results to her team.
134
+
135
+ As she developes the script she wants to pull data from a staging database and
136
+ just email the results to herself so she can see how the final product would
137
+ look without bothering the whole team until the finished product is ready.
138
+
139
+ Once everything is complete she wants to pull data from a production database
140
+ and send the email.
141
+
142
+ With Toggle, this is easy:
143
+
144
+ ```yaml
145
+ # Jane's sample config.yml
146
+ :development:
147
+ :who_to_email: 'jane@company.com'
148
+
149
+ :database:
150
+ :host: https://staging.data.company.com
151
+ :name: some_staging_db
152
+ :table: some_staging_table
153
+ :username: jane
154
+ :password: <%= ENV['DATABASE_PASSWORD'] %>
155
+
156
+ :production:
157
+ :who_to_email: 'team@company.com'
158
+
159
+ :database:
160
+ :host: https://prod.data.company.com
161
+ :name: some_prod_db
162
+ :table: some_prod_table
163
+ :username: jane
164
+ :password: <%= ENV['DATABASE_PASSWORD'] %>
165
+ ```
166
+
167
+ ```ruby
168
+ # Jane's sample email_data.rb script
169
+ require 'toggle'
170
+
171
+ toggle = Toggle.new config_filepath: './config.yml',
172
+ key: ENV['key']
173
+
174
+ connection = SomeDBDriver.connect host: toggle[:database][:host]
175
+ username: toggle[:database][:username]
176
+ password: toggle[:database][:password]
20
177
 
21
- TODO: Write usage instructions here
178
+ data = connection.get_data_from database: toggle[:database][:name],
179
+ table: toggle[:database][:table]
180
+
181
+ SomeEmailer.send to: toggle[:who_to_email],
182
+ what: data
183
+ ```
184
+
185
+ Now running `email_data.rb` under the development configuration settings is a
186
+ snap:
187
+
188
+ $ key=development ruby email_data.rb
189
+ # => will connect to the staging db + just email jane
190
+
191
+ And when it's deemed ready for primetime it can be run with the production
192
+ configuration settings via:
193
+
194
+ $ key=production ruby email_data.rb
195
+ # => will connect to the prod db + email the team
196
+
197
+ ## Realworld Use Case: Abstracted Configuration and Sharing
198
+
199
+ Continuing with our example from above, let's say that Jane needs to share the
200
+ script with John who is another developer on her team so he can work on it
201
+ (perhaps he wants to add in logic that does not send an email if no data is
202
+ returned so the team doesn't receive an empty email).
203
+
204
+ Jane can further abstract her `config.yml` file to faciliate quick sharing
205
+ between co-workers:
206
+
207
+ ```yaml
208
+ # Jane's new sample config.yml
209
+ #
210
+ # Notice that we have abstracted out the email address, database username and
211
+ # password into ENV vars
212
+ :development:
213
+ :who_to_email: <%= ENV['USER_EMAIL'] %>
214
+
215
+ :database:
216
+ :host: https://staging.data.company.com
217
+ :name: some_staging_db
218
+ :table: some_staging_table
219
+ :username: <%= ENV['DATABASE_USERNAME'] %>
220
+ :password: <%= ENV['DATABASE_PASSWORD'] %>
221
+
222
+ :production:
223
+ :who_to_email: 'team@company.com'
224
+
225
+ :database:
226
+ :host: https://prod.data.company.com
227
+ :name: some_prod_db
228
+ :table: some_prod_table
229
+ :username: <%= ENV['DATABASE_USERNAME'] %>
230
+ :password: <%= ENV['DATABASE_PASSWORD'] %>
231
+ ```
232
+
233
+ John is a `git clone` (or whatever vcs he is using) away from having the
234
+ script downloaded locally and ready to run without requiring any configuration
235
+ edits.
236
+
237
+ In fact, anyone that has `DATABASE_USERNAME`, `DATABASE_PASSWORD`
238
+ and `USER_EMAIL` set in their environment can run this script without requiring
239
+ any configuration adjustments.
240
+
241
+ In general if your team uses any common variables you should consider
242
+ abstracting each into environment variables and including them via ERB. Toggle
243
+ comes with an easy way to set this up on a per-computer basis. First, run:
244
+
245
+ $ toggle --init-local
246
+
247
+ This will create `~/.toggle.local`, which you can then edit to `export` any
248
+ variables you want to be available in your environment. Finally, make sure
249
+ you source this file so your variables are ready to go.
250
+
251
+ ## Ignoring the Config and Key Files
252
+
253
+ If you can effectively abstract out all configuration settings in environment
254
+ variables, you may be able to just commit your `config.yml` and your `key.yml`
255
+ files to source control.
256
+
257
+ However, consider .gitignore-ing each and providing a `config.yml.default` and
258
+ key.yml.default` in their place. With these default files in place you provide
259
+ runtime guidance, but allow each developer to make any local adjustments without
260
+ running the risk of having these changes committed back to the project's repo
261
+ and breaking someone else's settings when they pull in the latest changes.
262
+
263
+ Again, borrowing from the above example, if Jane were to instead provide
264
+ `config.yml.default` and `key.yml.default` files in her repo, anyone that
265
+ downloaded her repo would need to copy each file to their appropriate location
266
+ (`config.yml` and `key.yml` respectively) so the script could run. This can be
267
+ easily accomplished via:
268
+
269
+ $ toggle --copy-defaults project/path
270
+
271
+ or you can do this manually via:
272
+
273
+ $ cp project/path/config.yml.default project/project/config.yml
274
+ $ cp project/path/key.yml.default project/project/key.yml
275
+
276
+ ## Toggle CLI
277
+
278
+ Toggle comes bundled with a commandline interface:
279
+
280
+ ```
281
+ $ toggle --help
282
+ Usage: toggle <args>
283
+
284
+ Specific arguments:
285
+ -g, --init-local [PATH] Adds [PATH]/.toggle.local with var placeholders. Default is $HOME.
286
+ -k, --keys file Show available keys for the specified config FILE
287
+ --values FILE,KEY Show values for the KEY in the config FILE
288
+ --copy-config-defaults [PATH]
289
+ Copy all toggle config defaults to actuals in PATH. Default is pwd.
290
+ -c, --copy-defaults [PATH] Copy all .toggle.default to .toggle for PATH. Default is pwd.
291
+ --ensure-key [PATH] Copies the default key in [PATH] if actual key is not present, does nothing otherwise. Default [PATH] is pwd.
292
+ -m, --make-defaults [PATH] Create [PATH]/{config|key}{,.*}.default. Default is pwd.
293
+ -v, --version Show version
294
+ -h, --help Show this message
295
+ ```
22
296
 
23
297
  ## Contributing
24
298
 
25
- 1. Fork it
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create new Pull Request
299
+ If you would like to contribute code to Toggle you can do so through GitHub by
300
+ forking the repository and sending a pull request.
301
+
302
+ When submitting code, please make every effort to follow existing conventions
303
+ and style in order to keep the code as readable as possible.
304
+
305
+ Before your code can be accepted into the project you must also sign the
306
+ [Individual Contributor License Agreement (CLA)][1].
307
+
308
+
309
+ [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
310
+
311
+ ## License
312
+
313
+ Copyright 2013 Square Inc.
314
+
315
+ Licensed under the Apache License, Version 2.0 (the "License");
316
+ you may not use this file except in compliance with the License.
317
+ You may obtain a copy of the License at
318
+
319
+ http://www.apache.org/licenses/LICENSE-2.0
320
+
321
+ Unless required by applicable law or agreed to in writing, software
322
+ distributed under the License is distributed on an "AS IS" BASIS,
323
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
324
+ See the License for the specific language governing permissions and
325
+ limitations under the License.
data/Rakefile CHANGED
@@ -1 +1,28 @@
1
+ #!/usr/bin/env rake
1
2
  require "bundler/gem_tasks"
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |t|
7
+ t.rspec_opts = '-b'
8
+ end
9
+
10
+ task default: :spec
11
+ rescue LoadError
12
+ $stderr.puts "rspec not available, spec task not provided"
13
+ end
14
+
15
+ begin
16
+ require 'cane/rake_task'
17
+
18
+ desc "Run cane to check quality metrics"
19
+ Cane::RakeTask.new(:quality) do |cane|
20
+ cane.abc_max = 10
21
+ cane.style_glob = "lib/**/*.rb"
22
+ cane.no_doc = true
23
+ end
24
+
25
+ task :default => :quality
26
+ rescue LoadError
27
+ warn "cane not available, quality task not provided."
28
+ end
data/bin/toggle ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
4
+
5
+ require 'toggle'
6
+ require 'optparse'
7
+ require 'fileutils'
8
+
9
+ def default_key_filepath
10
+ File.expand_path('../../defaults/key.yml.default', __FILE__)
11
+ end
12
+
13
+ def default_config_filepath
14
+ File.expand_path('../../defaults/config.yml.default', __FILE__)
15
+ end
16
+
17
+ def wants_to_force_copy? current_file
18
+ respond_yes_to? "File #{current_file} exists. Replace?"
19
+ end
20
+
21
+ def actual_key_in path
22
+ find_file_in path, /^(key(\.yml))?$/
23
+ end
24
+
25
+ def actual_config_in path
26
+ find_file_in path, /^config\.yml$/
27
+ end
28
+
29
+ # finds the first file in "path" arg that matches "regex" pattern
30
+ # returns path + filename
31
+ def find_file_in path, regex
32
+ file = Dir[File.join(path, '*')].map { |filepath|
33
+ File.basename filepath
34
+ }.find { |filename|
35
+ filename =~ regex
36
+ }
37
+
38
+ file ? File.join(path, file) : nil
39
+ end
40
+
41
+ # Returns true if response starts with a 'y' or 'Y' (as in 'yes')
42
+ # Returns false if response starts with a 'n' or 'N' (as in 'no')
43
+ # Aborts if response starts with a 'q' or 'Q' (as in 'quit')
44
+ def respond_yes_to? prompt
45
+ print "#{prompt} (y/n/q) "
46
+ normalized_response = gets[0].chomp.downcase
47
+ normalized_response.eql?('q') ? abort('... quitting') : normalized_response.eql?('y')
48
+ end
49
+
50
+ # The reference file is guaranteed to exist
51
+ def identical_files? reference_file, other_file
52
+ File.exists?(other_file) && FileUtils.identical?(reference_file, other_file)
53
+ end
54
+
55
+ # Method requires a path and file_pattern and recursively searchs for matching
56
+ # files.
57
+ #
58
+ # When a match is found, method detects if the default is the same as the
59
+ # "actual" (the same filename just without the .default).
60
+ #
61
+ # If the files are identical, the method does nothing. If the actual file does
62
+ # not exist OR the user opts to clobber the existing, the default will replace
63
+ # the actual. Otherwise, the actual is left as is.
64
+ def recursively_copy_defaults path, file_pattern, options = {}
65
+ defaults = {attempt_force_copy: true}
66
+ options = defaults.merge(options)
67
+
68
+ attempt_force_copy = options[:attempt_force_copy]
69
+
70
+ Dir[File.join(path, '/**/', file_pattern)].each do |default_file|
71
+ file = default_file.slice(/(.*)\.default$/, 1)
72
+
73
+ if identical_files? default_file, file
74
+ puts "Default is identical to #{file}, skipping!"
75
+ elsif !File.exists?(file) || (attempt_force_copy && wants_to_force_copy?(file))
76
+ puts "Copying #{file} from default"
77
+ FileUtils.cp default_file, file
78
+ else
79
+ puts "Not changing #{file}"
80
+ end
81
+ end
82
+ end
83
+
84
+ opt_parser = OptionParser.new do |opts|
85
+ opts.banner = "Usage: toggle <args>"
86
+ opts.separator ""
87
+ opts.separator "Specific arguments:"
88
+
89
+ opts.on('-g', '--init-local [PATH]', String, 'Adds [PATH]/.toggle.local with var placeholders. Default is $HOME.') do |path|
90
+ path ||= ENV['HOME']
91
+
92
+ if path && path.empty?
93
+ raise RuntimeError, 'You must specify a PATH or have HOME env set!'
94
+ end
95
+
96
+ local_config = File.join(path, '.toggle.local')
97
+ sourcing_bash_instructions = "if [ -s ~/.toggle.local ] ; then source ~/.toggle.local ; fi"
98
+
99
+ if !File.exists?(local_config) || wants_to_force_copy?(local_config)
100
+ content = <<-EOS.strip_heredoc
101
+ # Add any variables that you'd like below.
102
+ #
103
+ # We've included a few suggestions, but please feel free
104
+ # to modify as needed.
105
+ #
106
+ # Make sure that you source this file in your ~/.bash_profile
107
+ # or ~/.bashrc (or whereever you'd like) via:
108
+ #
109
+ # #{sourcing_bash_instructions}
110
+ export DATABASE_HOST=''
111
+ export DATABASE_NAME=''
112
+ export DATABASE_USERNAME=''
113
+ export DATABASE_PASSWORD=''
114
+ export USER_EMAIL=''
115
+ EOS
116
+
117
+ %x(echo "#{content}" > #{local_config})
118
+ puts "Local toggle config added at #{local_config}"
119
+ puts "Now edit the file and source it from ~/.bash_profile or ~/.bashrc via: #{sourcing_bash_instructions}"
120
+ else
121
+ puts "Not changing #{local_config}"
122
+ end
123
+ end
124
+
125
+ opts.on("-k", "--keys file", String, "Show available keys for the specified config FILE") do |file|
126
+ opts.banner = "Usage: toggle --keys FILE"
127
+
128
+ if File.exists? file
129
+ toggle = Toggle::Compiler.new(file).parsed_content
130
+ puts toggle.keys.map{|k| "- #{k}"}.join("\n")
131
+ else
132
+ puts "toggle config file not found, please check specified path"
133
+ end
134
+ end
135
+
136
+ # TODO: remove yaml preamble
137
+ opts.on("--values FILE,KEY", Array, "Show values for the KEY in the config FILE") do |params|
138
+ opts.banner = "Usage: toggle --values FILE,KEY"
139
+
140
+ if File.exists?(file = params[0])
141
+ toggle = Toggle::Compiler.new(file).parsed_content
142
+ if toggle.keys.include?(key = params[1].to_sym)
143
+ puts toggle[key].to_yaml.gsub(/(:password:).+/, "\\1 [redacted]")
144
+ else
145
+ puts "#{key} not found in #{file}"
146
+ end
147
+ else
148
+ puts "toggle config file not found, please check specified path"
149
+ end
150
+ end
151
+
152
+ opts.on('--copy-config-defaults [PATH]', String, 'Copy all toggle config defaults to actuals in PATH. Default is pwd.') do |path|
153
+ path ||= Dir.pwd
154
+ recursively_copy_defaults(path, 'config{,.*}.default')
155
+ end
156
+
157
+ opts.on('-c', '--copy-defaults [PATH]', String, 'Copy all .toggle.default to .toggle for PATH. Default is pwd.') do |path|
158
+ path ||= Dir.pwd
159
+ recursively_copy_defaults(path, '{key,config}{,.*}.default')
160
+ end
161
+
162
+ opts.on('--ensure-key [PATH]', String, 'Copies the default key in [PATH] if actual key is not present, does nothing otherwise. Default [PATH] is pwd.') do |path|
163
+ path ||= Dir.pwd
164
+ recursively_copy_defaults(path, 'key{,.*}.default', attempt_force_copy: false)
165
+ end
166
+
167
+ opts.on('-m', '--make-defaults [PATH]', String, 'Create [PATH]/{config|key}{,.*}.default. Default is pwd.') do |path|
168
+ path ||= Dir.pwd
169
+
170
+ default_key = actual_key_in(path) || default_key_filepath
171
+ default_config = actual_config_in(path) || default_config_filepath
172
+
173
+ key_destination = File.join(path, 'key.yml.default')
174
+ config_destination = File.join(path, 'config.yml.default')
175
+
176
+ if !File.exists?(key_destination) || wants_to_force_copy?(key_destination)
177
+ FileUtils.cp(default_key, key_destination) unless identical_files?(default_key, key_destination)
178
+ puts "Default key written to #{key_destination}"
179
+ puts "Now go edit it!"
180
+ end
181
+
182
+ if !File.exists?(config_destination) || wants_to_force_copy?(config_destination)
183
+ FileUtils.cp(default_config, config_destination) unless identical_files?(default_config, key_destination)
184
+ puts "Default config written to #{config_destination}"
185
+ puts "Now go edit it!"
186
+ end
187
+ end
188
+
189
+ opts.on_tail("-v", "--version", "Show version") do
190
+ puts "Toggle version #{Toggle::VERSION}"
191
+ exit
192
+ end
193
+
194
+ opts.on_tail("-h", "--help", "Show this message") do
195
+ puts opts
196
+ exit
197
+ end
198
+ end
199
+
200
+ opt_parser.parse!