simple_scripting 0.9.1 → 0.9.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 59993d0d200b77aa1deb2aadda05560b427cee00
4
- data.tar.gz: 1baeb57678e05f4fac32cf8d34afb42b175d5b08
3
+ metadata.gz: f48ef28748f34f32e008d951c4b4d7471e5cbe4b
4
+ data.tar.gz: e6cd1cd8132eb9df036e0b5aff06ab0f8e2dd1ec
5
5
  SHA512:
6
- metadata.gz: a06e70451f2b78d8833edaa7649c7ac9130920aca97b2cd9ec171ca3db30a370a65fd15a120a05256f29289bc3e66cad8202c8a8e0f4ea3c04c47ce7ba09d0a3
7
- data.tar.gz: 52cf1615fa2ca5b6f8c1147176ba3859cccc856b613fba1a178d21185bd826f9005cd1d6d88d6fd602e3d8646c267dab4d2a7321357a3c7fe8563c0869821363
6
+ metadata.gz: fa2f35c9e810e06a1e29125650d071de636e4bb80ce3d9010a8eb74e006646f0a786cdab9311a33e0fb77d33d541c55724a78bb9ffb607a199db952dc7f28819
7
+ data.tar.gz: 4e2bdc8da33f0aeede34d91dffeff61f2e420ab633d45cd408519f60becd026d35d13f0139af655c035074792626abae88e8041b1b95bf83e0b5f376f928a946
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  .byebug_history
2
+ coverage/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/Gemfile CHANGED
@@ -8,4 +8,5 @@ end
8
8
 
9
9
  group :test do
10
10
  gem 'rspec', '~> 3.6'
11
+ gem 'coveralls', '~> 0.8.21', require: false
11
12
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,15 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
+ coveralls (0.8.21)
5
+ json (>= 1.8, < 3)
6
+ simplecov (~> 0.14.1)
7
+ term-ansicolor (~> 1.3)
8
+ thor (~> 0.19.4)
9
+ tins (~> 1.6)
4
10
  diff-lcs (1.3)
11
+ docile (1.1.5)
12
+ json (2.1.0)
5
13
  parseconfig (1.0.8)
6
14
  rake (12.0.0)
7
15
  rspec (3.6.0)
@@ -17,11 +25,21 @@ GEM
17
25
  diff-lcs (>= 1.2.0, < 2.0)
18
26
  rspec-support (~> 3.6.0)
19
27
  rspec-support (3.6.0)
28
+ simplecov (0.14.1)
29
+ docile (~> 1.1.0)
30
+ json (>= 1.8, < 3)
31
+ simplecov-html (~> 0.10.0)
32
+ simplecov-html (0.10.2)
33
+ term-ansicolor (1.6.0)
34
+ tins (~> 1.0)
35
+ thor (0.19.4)
36
+ tins (1.15.0)
20
37
 
21
38
  PLATFORMS
22
39
  ruby
23
40
 
24
41
  DEPENDENCIES
42
+ coveralls (~> 0.8.21)
25
43
  parseconfig (~> 1.0)
26
44
  rake (~> 12.0)
27
45
  rspec (~> 3.6)
data/README.md CHANGED
@@ -1,4 +1,8 @@
1
+ [![Gem Version][GV img]](https://rubygems.org/gems/simple_scripting)
1
2
  [![Build Status][BS img]](https://travis-ci.org/saveriomiroddi/simple_scripting)
3
+ [![Dependency Status][DS img]](https://gemnasium.com/saveriomiroddi/simple_scripting)
4
+ [![Code Climate][CC img]](https://codeclimate.com/github/saveriomiroddi/simple_scripting)
5
+ [![Coverage Status][CS img]](https://coveralls.io/r/saveriomiroddi/simple_scripting)
2
6
 
3
7
  # SimpleScripting
4
8
 
@@ -23,7 +27,7 @@ This is a definition example:
23
27
  'schedule',
24
28
  '[weeks]',
25
29
  long_help: 'This is the long help! It can span multiple lines.'
26
- )
30
+ ) || exit
27
31
 
28
32
  which:
29
33
 
@@ -33,7 +37,7 @@ which:
33
37
  - optionally accepts the `weeks` argument,
34
38
  - automatically adds the `-h` and `--help` switches,
35
39
  - prints all the options and the long help if the help is invoked,
36
- - prints the help and exits if invalid parameters are passed (eg. too many).
40
+ - exits with a descriptive error if invalid parameters are passed.
37
41
 
38
42
  This is a sample result:
39
43
 
@@ -90,8 +94,12 @@ This is the workflow and functionality offered by `SS::C`:
90
94
 
91
95
  The purpose of encryption in this library is just to avoid displaying passwords in plaintext; it's not considered safe against attacks.
92
96
 
93
- [BS img]: https://travis-ci.org/saveriomiroddi/simple_scripting.svg?branch=master
94
-
95
97
  ## Help
96
98
 
97
99
  See the [wiki](https://github.com/saveriomiroddi/simple_scripting/wiki) for additional help.
100
+
101
+ [GV img]: https://badge.fury.io/rb/simple_scripting.png
102
+ [BS img]: https://travis-ci.org/saveriomiroddi/simple_scripting.svg?branch=master
103
+ [DS img]: https://gemnasium.com/saveriomiroddi/simple_scripting.png
104
+ [CC img]: https://codeclimate.com/github/saveriomiroddi/simple_scripting.png
105
+ [CS img]: https://coveralls.io/repos/saveriomiroddi/simple_scripting/badge.png?branch=master
@@ -4,129 +4,200 @@ module SimpleScripting
4
4
 
5
5
  module Argv
6
6
 
7
+ class ExitError < StandardError; end
8
+
7
9
  extend self
8
10
 
9
- def decode(*params_definition, arguments: ARGV, long_help: nil, output: $stdout, command: nil)
10
- # If the param is a Hash, we have multiple commands. We check and if the command is correct,
11
- # recursively call the function with the specific parameters.
11
+ def decode(*params_definition, arguments: ARGV, long_help: nil, output: $stdout)
12
+ # WATCH OUT! @long_help can also be set in :decode_command!. See issue #17.
12
13
  #
14
+ @long_help = long_help
15
+ @output = output
16
+
13
17
  if params_definition.first.is_a?(Hash)
14
- command = arguments.shift
15
- commands_definition = params_definition.first
18
+ decode_command!(params_definition, arguments)
19
+ else
20
+ decode_arguments!(params_definition, arguments)
21
+ end
22
+ rescue ExitError
23
+ # return nil, to be used with the 'decode(...) || exit' pattern
24
+ ensure
25
+ # Clean up the instance variables.
26
+ #
27
+ # There is a balance to strike between instance variables, and local variables
28
+ # passed around. One of the options, which is this case, is to set and instance
29
+ # variables only these two, which are constant.
16
30
 
17
- if command == '-h' || command == '--help'
18
- print_optparse_commands_help(commands_definition, output, false)
19
- output == $stdout ? exit : return
20
- end
31
+ @long_help = nil
32
+ @output = nil
33
+ end
21
34
 
22
- command_params_definition = commands_definition[command]
35
+ private
23
36
 
24
- if command_params_definition.nil?
25
- print_optparse_commands_help(commands_definition, output, true)
26
- output == $stdout ? exit : return
27
- else
28
- return [command, decode(*command_params_definition, arguments: arguments, output: output, command: command)]
37
+ # MAIN CASES ###########################################
38
+
39
+ # Input params_definition for a non-nested case:
40
+ #
41
+ # [{"command1"=>["arg1", {:long_help=>"This is the long help."}], "command2"=>["arg2"]}]
42
+ #
43
+ def decode_command!(params_definition, arguments, commands_stack=[])
44
+ commands_definition = params_definition.first
45
+
46
+ # Set the `command` variable only after; in the case where we print the help, this variable
47
+ # must be unset.
48
+ #
49
+ command_for_check = arguments.shift
50
+
51
+ if command_for_check == '-h' || command_for_check == '--help'
52
+ print_optparse_commands_help(nil, commands_definition)
53
+ end
54
+
55
+ command = command_for_check
56
+ command_params_definition = commands_definition[command]
57
+
58
+ case command_params_definition
59
+ when nil
60
+ print_optparse_commands_help(command, commands_definition)
61
+ when Hash
62
+ commands_stack << command
63
+
64
+ # Nested case! Decode recursively
65
+ #
66
+ decode_command!([command_params_definition], arguments, commands_stack)
67
+ else
68
+ commands_stack << command
69
+
70
+ if command_params_definition.last.is_a?(Hash)
71
+ internal_params = command_params_definition.pop # only long_help is here, if present
72
+ @long_help = internal_params.delete(:long_help)
29
73
  end
74
+
75
+ [
76
+ command,
77
+ decode_arguments!(command_params_definition, arguments, commands_stack),
78
+ ]
30
79
  end
80
+ end
31
81
 
82
+ def decode_arguments!(params_definition, arguments, commands_stack=[])
32
83
  result = {}
33
- parser_opts_ref = nil # not available outside the block
84
+ parser_opts_copy = nil # not available outside the block
34
85
  args = {} # { 'name' => mandatory? }
35
86
 
36
87
  OptionParser.new do |parser_opts|
37
88
  params_definition.each do |param_definition|
38
89
  case param_definition
39
90
  when Array
40
- if param_definition[1] && param_definition[1].start_with?('--')
41
- key = param_definition[1].split(' ')[0][2 .. -1].gsub('-', '_').to_sym
42
- else
43
- key = param_definition[0][1 .. -1].to_sym
44
- end
45
-
46
- parser_opts.on(*param_definition) do |value|
47
- result[key] = value || true
48
- end
91
+ process_option_definition!(param_definition, parser_opts, result)
49
92
  when String
50
- if param_definition.start_with?('[')
51
- arg_name = param_definition[1 .. -2].to_sym
52
-
53
- args[arg_name] = false
54
- else
55
- arg_name = param_definition.to_sym
56
-
57
- args[arg_name] = true
58
- end
93
+ process_argument_definition!(param_definition, args)
59
94
  else
60
95
  raise "Unrecognized value: #{param_definition}"
61
96
  end
62
97
  end
63
98
 
64
99
  parser_opts.on('-h', '--help', 'Help') do
65
- print_optparse_help(parser_opts, args, long_help, output, command: command)
66
- output == $stdout ? exit : return
100
+ print_optparse_arguments_help(commands_stack, args, parser_opts_copy)
67
101
  end
68
102
 
69
- parser_opts_ref = parser_opts
103
+ parser_opts_copy = parser_opts
70
104
  end.parse!(arguments)
71
105
 
72
106
  first_arg_name = args.keys.first.to_s
73
107
 
74
- # Varargs
75
108
  if first_arg_name.start_with?('*')
76
- # Mandatory?
77
- if args.fetch(first_arg_name.to_sym)
78
- if arguments.empty?
79
- print_optparse_help(parser_opts_ref, args, long_help, output, command: command)
80
- output == $stdout ? exit : return
81
- else
82
- name = args.keys.first[1..-1].to_sym
109
+ process_varargs!(arguments, result, commands_stack, args, parser_opts_copy)
110
+ else
111
+ process_regular_argument!(arguments, result, commands_stack, args, parser_opts_copy)
112
+ end
83
113
 
84
- result[name] = arguments
85
- end
86
- # Optional
114
+ result
115
+ end
116
+
117
+ # DEFINITIONS PROCESSING ###############################
118
+
119
+ def process_option_definition!(param_definition, parser_opts, result)
120
+ if param_definition[1] && param_definition[1].start_with?('--')
121
+ key = param_definition[1].split(' ')[0][2 .. -1].tr('-', '_').to_sym
122
+ else
123
+ key = param_definition[0][1 .. -1].to_sym
124
+ end
125
+
126
+ parser_opts.on(*param_definition) do |value|
127
+ result[key] = value || true
128
+ end
129
+ end
130
+
131
+ def process_argument_definition!(param_definition, args)
132
+ if param_definition.start_with?('[')
133
+ arg_name = param_definition[1 .. -2].to_sym
134
+
135
+ args[arg_name] = false
136
+ else
137
+ arg_name = param_definition.to_sym
138
+
139
+ args[arg_name] = true
140
+ end
141
+ end
142
+
143
+ def process_varargs!(arguments, result, commands_stack, args, parser_opts_copy)
144
+ first_arg_name = args.keys.first.to_s
145
+
146
+ # Mandatory argument
147
+ if args.fetch(first_arg_name.to_sym)
148
+ if arguments.empty?
149
+ print_optparse_arguments_help(commands_stack, args, parser_opts_copy)
87
150
  else
88
151
  name = args.keys.first[1..-1].to_sym
89
152
 
90
153
  result[name] = arguments
91
154
  end
155
+ # Optional
92
156
  else
93
- min_args_size = args.count { |name, mandatory| mandatory }
157
+ name = args.keys.first[1..-1].to_sym
94
158
 
95
- case arguments.size
96
- when (min_args_size .. args.size)
97
- arguments.zip(args) do |value, (name, mandatory)|
98
- result[name] = value
99
- end
100
- else
101
- print_optparse_help(parser_opts_ref, args, long_help, output, command: command)
102
- output == $stdout ? exit : return
103
- end
159
+ result[name] = arguments
104
160
  end
161
+ end
105
162
 
106
- result
163
+ def process_regular_argument!(arguments, result, commands_stack, args, parser_opts_copy)
164
+ min_args_size = args.count { |_, mandatory| mandatory }
165
+
166
+ case arguments.size
167
+ when (min_args_size .. args.size)
168
+ arguments.zip(args) do |value, (name, _)|
169
+ result[name] = value
170
+ end
171
+ else
172
+ print_optparse_arguments_help(commands_stack, args, parser_opts_copy)
173
+ end
107
174
  end
108
175
 
109
- private
176
+ # HELP #################################################
177
+
178
+ def print_optparse_commands_help(command, commands_definition)
179
+ @output.print "Invalid command. " if command
180
+ @output.puts "Valid commands:", "", " " + commands_definition.keys.join(', ')
110
181
 
111
- def print_optparse_commands_help(commands_definition, output, is_error)
112
- output.print "Invalid command. " if is_error
113
- output.puts "Valid commands:", "", " " + commands_definition.keys.join(', ')
182
+ raise ExitError
114
183
  end
115
184
 
116
- def print_optparse_help(parser_opts, args, long_help, output, command: nil)
117
- parser_opts_help = parser_opts.to_s
185
+ def print_optparse_arguments_help(commands_stack, args, parser_opts_copy)
186
+ parser_opts_help = parser_opts_copy.to_s
118
187
 
119
- if command
120
- parser_opts_help = parser_opts_help.sub!(/(\[options\])/, "#{command} \\1")
188
+ if commands_stack.size > 0
189
+ parser_opts_help = parser_opts_help.sub!('[options]', commands_stack.join(' ') + ' [options]')
121
190
  end
122
191
 
123
192
  if args.size > 0
124
193
  args_display = args.map { |name, mandatory| mandatory ? "<#{ name }>" : "[<#{ name }>]" }.join(' ')
125
- parser_opts_help = parser_opts_help.sub!(/^(Usage: .*)/, "\\1 #{args_display}")
194
+ parser_opts_help = parser_opts_help.sub!(/^(Usage: .*)/) { |text| "#{text} #{args_display}" }
126
195
  end
127
196
 
128
- output.puts parser_opts_help
129
- output.puts "", long_help if long_help
197
+ @output.puts parser_opts_help
198
+ @output.puts "", @long_help if @long_help
199
+
200
+ raise ExitError
130
201
  end
131
202
 
132
203
  end
@@ -1,5 +1,5 @@
1
1
  module SimpleScripting
2
2
 
3
- VERSION = "0.9.1"
3
+ VERSION = "0.9.2"
4
4
 
5
5
  end
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
20
20
 
21
21
  s.add_development_dependency "rake", "~> 12.0"
22
22
  s.add_development_dependency "rspec", "~> 3.6"
23
+ s.add_development_dependency 'coveralls', "~> 0.8.21"
23
24
 
24
25
  s.files = `git ls-files`.split("\n")
25
26
  s.test_files = `git ls-files -- spec/*`.split("\n")
@@ -26,7 +26,7 @@ describe SimpleScripting::Argv do
26
26
  it 'should implement the help' do
27
27
  decoder_params.last[:arguments] = ['-h']
28
28
 
29
- described_class.decode(*decoder_params)
29
+ return_value = described_class.decode(*decoder_params)
30
30
 
31
31
  expected_output = %Q{\
32
32
  Usage: rspec [options] <mandatory> [<optional>]
@@ -42,6 +42,7 @@ This is the long help!
42
42
  }
43
43
 
44
44
  expect(output_buffer.string).to eql(expected_output)
45
+ expect(return_value).to be(nil)
45
46
  end
46
47
 
47
48
  it "should implement basic switches and arguments (all set)" do
@@ -145,13 +146,14 @@ This is the long help!
145
146
 
146
147
  end
147
148
 
148
- describe 'Multiple commands' do
149
+ describe 'Commands' do
149
150
 
150
151
  describe 'regular case' do
151
152
 
152
153
  let(:decoder_params) {{
153
154
  'command1' => [
154
- 'arg1'
155
+ 'arg1',
156
+ long_help: 'This is the long help.'
155
157
  ],
156
158
  'command2' => [
157
159
  'arg2'
@@ -205,6 +207,8 @@ $a = true
205
207
  expected_output = %Q{\
206
208
  Usage: rspec command1 [options] <arg1>
207
209
  -h, --help Help
210
+
211
+ This is the long help.
208
212
  }
209
213
 
210
214
  expect(output_buffer.string).to eql(expected_output)
@@ -212,6 +216,54 @@ Usage: rspec command1 [options] <arg1>
212
216
 
213
217
  end
214
218
 
219
+ describe 'Nested commands' do
220
+
221
+ let(:decoder_params) {{
222
+ 'command1' => {
223
+ 'nested1a' => [
224
+ 'arg1',
225
+ long_help: 'nested1a long help.'
226
+ ],
227
+ 'nested1b' => [
228
+ 'arg1b'
229
+ ],
230
+ },
231
+ 'command2' => [
232
+ 'arg2'
233
+ ],
234
+ output: output_buffer
235
+ }}
236
+
237
+ it 'should print the command1 help' do
238
+ decoder_params[:arguments] = ['command1', '-h']
239
+
240
+ actual_result = described_class.decode(decoder_params)
241
+
242
+ expected_output = "\
243
+ Valid commands:
244
+
245
+ nested1a, nested1b
246
+ "
247
+
248
+ expect(output_buffer.string).to eql(expected_output)
249
+ end
250
+
251
+ it 'should print the nested1a help, and long help' do
252
+ decoder_params[:arguments] = ['command1', 'nested1a', '-h']
253
+
254
+ actual_result = described_class.decode(decoder_params)
255
+
256
+ expected_output = "\
257
+ Usage: rspec command1 nested1a [options] <arg1>
258
+ -h, --help Help
259
+
260
+ nested1a long help.
261
+ "
262
+
263
+ expect(output_buffer.string).to eql(expected_output)
264
+ end
265
+ end
266
+
215
267
  describe 'No argv case' do
216
268
 
217
269
  let(:decoder_params) {{
@@ -0,0 +1,51 @@
1
+ # Coverage setup
2
+
3
+ require 'coveralls'
4
+ Coveralls.wear!
5
+
6
+ # This file was generated by the `rspec --init` command. Conventionally, all
7
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
8
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
9
+ # this file to always be loaded, without a need to explicitly require it in any
10
+ # files.
11
+ #
12
+ # Given that it is always loaded, you are encouraged to keep this file as
13
+ # light-weight as possible. Requiring heavyweight dependencies from this file
14
+ # will add to the boot time of your test suite on EVERY test run, even for an
15
+ # individual file that may not need all of that loaded. Instead, consider making
16
+ # a separate helper file that requires the additional dependencies and performs
17
+ # the additional setup, and require it from the spec files that actually need
18
+ # it.
19
+ #
20
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
21
+ RSpec.configure do |config|
22
+ # rspec-expectations config goes here. You can use an alternate
23
+ # assertion/expectation library such as wrong or the stdlib/minitest
24
+ # assertions if you prefer.
25
+ config.expect_with :rspec do |expectations|
26
+ # This option will default to `true` in RSpec 4. It makes the `description`
27
+ # and `failure_message` of custom matchers include text for helper methods
28
+ # defined using `chain`, e.g.:
29
+ # be_bigger_than(2).and_smaller_than(4).description
30
+ # # => "be bigger than 2 and smaller than 4"
31
+ # ...rather than:
32
+ # # => "be bigger than 2"
33
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
34
+ end
35
+
36
+ # rspec-mocks config goes here. You can use an alternate test double
37
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
38
+ config.mock_with :rspec do |mocks|
39
+ # Prevents you from mocking or stubbing a method that does not exist on
40
+ # a real object. This is generally recommended, and will default to
41
+ # `true` in RSpec 4.
42
+ mocks.verify_partial_doubles = true
43
+ end
44
+
45
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
46
+ # have no way to turn it off -- the option exists only for backwards
47
+ # compatibility in RSpec 3). It causes shared context metadata to be
48
+ # inherited by the metadata hash of host groups and examples, rather than
49
+ # triggering implicit auto-inclusion in groups with matching metadata.
50
+ config.shared_context_metadata_behavior = :apply_to_host_groups
51
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_scripting
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saverio Miroddi
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: coveralls
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.8.21
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.8.21
55
69
  description: Simplifies options parsing and configuration loading.
56
70
  email:
57
71
  - saverio.pub2@gmail.com
@@ -60,6 +74,7 @@ extensions: []
60
74
  extra_rdoc_files: []
61
75
  files:
62
76
  - ".gitignore"
77
+ - ".rspec"
63
78
  - ".travis.yml"
64
79
  - Gemfile
65
80
  - Gemfile.lock
@@ -74,6 +89,7 @@ files:
74
89
  - spec/simple_scripting/argv_spec.rb
75
90
  - spec/simple_scripting/configuration/value_spec.rb
76
91
  - spec/simple_scripting/configuration_spec.rb
92
+ - spec/spec_helper.rb
77
93
  homepage: https://github.com/saveriomiroddi/simple_scripting
78
94
  licenses:
79
95
  - GPL-3.0
@@ -94,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
110
  version: '0'
95
111
  requirements: []
96
112
  rubyforge_project:
97
- rubygems_version: 2.5.2
113
+ rubygems_version: 2.6.13
98
114
  signing_key:
99
115
  specification_version: 4
100
116
  summary: Library for simplifying some typical scripting functionalities.
@@ -102,3 +118,4 @@ test_files:
102
118
  - spec/simple_scripting/argv_spec.rb
103
119
  - spec/simple_scripting/configuration/value_spec.rb
104
120
  - spec/simple_scripting/configuration_spec.rb
121
+ - spec/spec_helper.rb