toggle 0.0.0.alpha → 1.0.0.rc

Sign up to get free protection for your applications and to get access to all the features.
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!