command_line_boss 0.1.0

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.
@@ -0,0 +1,239 @@
1
+ Feature: CreateSpreadsheet::Cli#parse
2
+
3
+ Parse the create-spreadsheet command line
4
+
5
+ Example: Nothing is given on the command line
6
+ Given the command line ""
7
+ When the command line is parsed
8
+ Then the parser should have succeeded
9
+ And the title attribute should be nil
10
+ And the sheets attribute should be empty
11
+ And the permissions attribute should be empty
12
+ And the folder_id attribute should be nil
13
+
14
+ Example: A spreadsheet title is given
15
+ Given the command line "MySpreadsheet"
16
+ When the command line is parsed
17
+ Then the parser should have succeeded
18
+ And the title attribute should be "MySpreadsheet"
19
+
20
+ Example: A sheet is defined with a title
21
+ Given the command line "--sheet=Summary"
22
+ When the command line is parsed
23
+ Then the parser should have succeeded
24
+ And the sheets attribute should contain the following Sheets:
25
+ """
26
+ [
27
+ {
28
+ title: "Summary",
29
+ data: nil
30
+ }
31
+ ]
32
+ """
33
+
34
+ Example: A sheet is defined with a title and data
35
+ Given the command line "--sheet=Summary --data=data1.csv"
36
+ And a file "data1.csv" containing:
37
+ """
38
+ 1,2,3
39
+ 4,5,6
40
+ """
41
+ When the command line is parsed
42
+ Then the parser should have succeeded
43
+ And the sheets attribute should contain the following Sheets:
44
+ """
45
+ [
46
+ {
47
+ "title": "Summary",
48
+ "data": [["1", "2", "3"], ["4", "5", "6"]]
49
+ }
50
+ ]
51
+ """
52
+
53
+ Example: Multiple sheets are defined both with data
54
+ Given the command line "--sheet=Summary --data=summary.csv --sheet=Detail --data=detail.csv"
55
+ And a file "summary.csv" containing:
56
+ """
57
+ 6
58
+ """
59
+ And a file "detail.csv" containing:
60
+ """
61
+ 1
62
+ 2
63
+ 3
64
+ """
65
+ When the command line is parsed
66
+ Then the parser should have succeeded
67
+ And the sheets attribute should contain the following Sheets:
68
+ """
69
+ [
70
+ { "title": "Summary", "data": [["6"]] },
71
+ { "title": "Detail", "data": [["1"], ["2"], ["3"]] }
72
+ ]
73
+ """
74
+
75
+ Example: Multiple sheets are defined only one with data
76
+ Given the command line "--sheet=Summary --sheet=Detail --data=detail.csv"
77
+ And a file "detail.csv" containing:
78
+ """
79
+ Name,Age
80
+ John,25
81
+ Jane,23
82
+ """
83
+ When the command line is parsed
84
+ Then the parser should have succeeded
85
+ And the sheets attribute should contain the following Sheets:
86
+ """
87
+ [
88
+ { title: "Summary", data: nil },
89
+ { title: "Detail", data: [["Name", "Age"], ["John", "25"], ["Jane", "23"]] }
90
+ ]
91
+ """
92
+
93
+ Example: A user permission is given
94
+ Given the command line "--permission=user:bob@example.com:reader"
95
+ When the command line is parsed
96
+ Then the parser should have succeeded
97
+ And the permissions attribute should contain the following Permissions:
98
+ """
99
+ [
100
+ {
101
+ "permission_spec": "user:bob@example.com:reader",
102
+ "type": "user", "subject": "bob@example.com", "role": "reader"
103
+ }
104
+ ]
105
+ """
106
+
107
+ Example: A group permission is given
108
+ Given the command line "--permission=group:admins@example.com:writer"
109
+
110
+ When the command line is parsed
111
+ Then the parser should have succeeded
112
+ And the permissions attribute should contain the following Permissions:
113
+ """
114
+ [
115
+ {
116
+ "permission_spec": "group:admins@example.com:writer",
117
+ "type": "group", "subject": "admins@example.com", "role": "writer"
118
+ }
119
+ ]
120
+ """
121
+
122
+ Example: A domain permission is given
123
+ Given the command line "--permission=domain:domain_name:reader"
124
+ When the command line is parsed
125
+ Then the parser should have succeeded
126
+ And the permissions attribute should contain the following Permissions:
127
+ """
128
+ [
129
+ {
130
+ "permission_spec": "domain:domain_name:reader",
131
+ "type": "domain", "subject": "domain_name", "role": "reader"
132
+ }
133
+ ]
134
+ """
135
+
136
+ Example: An anyone permission is given
137
+ Given the command line "--permission=anyone:reader"
138
+ When the command line is parsed
139
+ Then the parser should have succeeded
140
+ And the permissions attribute should contain the following Permissions:
141
+ """
142
+ [
143
+ {
144
+ permission_spec: "anyone:reader", type: "anyone", subject: nil, role: "reader"
145
+ }
146
+ ]
147
+ """
148
+
149
+ Example: Multiple permissions are given
150
+ Given the command line "--permission=user:bob@example.com:writer --permission=anyone:reader"
151
+ When the command line is parsed
152
+ Then the parser should have succeeded
153
+ And the permissions attribute should contain the following Permissions:
154
+ """
155
+ [
156
+ {
157
+ permission_spec: "user:bob@example.com:writer", type: "user", subject: "bob@example.com", role: "writer"
158
+ },
159
+ {
160
+ permission_spec: "anyone:reader", type: "anyone", subject: nil, role: "reader"
161
+ }
162
+ ]
163
+ """
164
+
165
+ Example: A folder is given
166
+ Given the command line "--folder=0ALLuhm2AwwlJUk9PVA"
167
+ When the command line is parsed
168
+ Then the parser should have succeeded
169
+ And the folder_id attribute should be "0ALLuhm2AwwlJUk9PVA"
170
+
171
+ # Failure cases
172
+
173
+ Example: A sheet given without a name
174
+ Given the command line "--sheet"
175
+ When the command line is parsed
176
+ Then the parser should have failed with the error "ERROR: missing argument: --sheet"
177
+
178
+ Example: A permission is given without a permission spec
179
+ Given the command line "--permission"
180
+ When the command line is parsed
181
+ Then the parser should have failed with the error "ERROR: missing argument: --permission"
182
+
183
+ Example: A invalid permission is given
184
+ Given the command line "--permission=anyone-writer"
185
+ When the command line is parsed
186
+ Then the parser should have failed with the error "ERROR: Invalid permission: anyone-writer"
187
+
188
+ Example: The permission spec has an invalid type
189
+ Given the command line "--permission=invalid:test@example.com:reader"
190
+ When the command line is parsed
191
+ Then the parser should have failed with the error "ERROR: Invalid permission type: invalid"
192
+
193
+ Example: A permission spec has an invalid role
194
+ Given the command line "--permission=user:test@example.com:invalid"
195
+ When the command line is parsed
196
+ Then the parser should have failed with the error "ERROR: Invalid permission role: invalid"
197
+
198
+ Example: A subject is given for an anyone permission
199
+ Given the command line "--permission=anyone:test@example.com:reader"
200
+ When the command line is parsed
201
+ Then the parser should have failed with the error "ERROR: An anyone permission must not have a subject"
202
+
203
+ Example: A subject is not given for a user permission
204
+ Given the command line "--permission=user:writer"
205
+ When the command line is parsed
206
+ Then the parser should have failed with the error "ERROR: A user permission must have a subject"
207
+
208
+ Example: Data is given without a path
209
+ Given the command line "--sheet=Summary --data"
210
+ When the command line is parsed
211
+ Then the parser should have failed with the error "ERROR: missing argument: --data"
212
+
213
+ Example: Data is given with an non-existant path
214
+ Given the command line "--sheet=Summary --data=nonexistent.csv"
215
+ And the file "nonexistent.csv" does not exist
216
+ When the command line is parsed
217
+ Then the parser should have failed with the error "ERROR: Data file not found: nonexistent.csv"
218
+
219
+ Example: The folder option is given twice
220
+ Given the command line "--folder=0ALLuhm2AwwlJUk9PVA --folder=0ALLuhm2AwwlJUk9PVA"
221
+ When the command line is parsed
222
+
223
+ Example: A same sheet name is given twice
224
+ Given the command line "--sheet=Summary --sheet=Summary"
225
+ When the command line is parsed
226
+ Then the parser should have failed with the error "ERROR: The sheet Summary was given more than once"
227
+
228
+ Example: Data is given twice for the same sheet
229
+ Given the command line "--sheet=Summary --data=data1.csv --data=data2.csv"
230
+ And a file "data1.csv" containing:
231
+ """
232
+ 1
233
+ """
234
+ And a file "data2.csv" containing:
235
+ """
236
+ 2
237
+ """
238
+ When the command line is parsed
239
+ Then the parser should have failed with the error "ERROR: Only one data file is allowed per sheet"
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ step 'the command line :command_line' do |command_line|
4
+ @args = command_line.split
5
+ end
6
+
7
+ # step 'the command line is parsed' do
8
+ # @options = CreateSpreadsheetCli.new.parse(@args)
9
+ # end
10
+
11
+ step 'the command line is parsed' do
12
+ stdout = StringIO.new
13
+ stderr = StringIO.new
14
+ original_stdout = $stdout
15
+ original_stderr = $stderr
16
+
17
+ begin
18
+ $stdout = stdout
19
+ $stderr = stderr
20
+ @options = CreateSpreadsheet::CommandLine.new.parse(@args)
21
+ rescue StandardError => e
22
+ @error = e
23
+ @options = e.parser
24
+ ensure
25
+ $stdout = original_stdout
26
+ $stderr = original_stderr
27
+ end
28
+
29
+ @stdout = stdout.string
30
+ @stderr = stderr.string
31
+ end
32
+
33
+ step 'the parser should have succeeded' do
34
+ expect(@options.error_messages).to eq([])
35
+ expect(@error).to be_nil
36
+ expect(@stdout).to be_empty
37
+ expect(@stderr).to be_empty
38
+ end
39
+
40
+ step 'the parser should have failed' do
41
+ expect(@options.error_messages).not_to be_empty
42
+ expect(@error).to be_nil
43
+ expect(@stdout).to be_empty
44
+ expect(@stderr).to be_empty
45
+ end
46
+
47
+ step 'the parser should have failed with the error :message' do |message|
48
+ expect(@options.error_messages).not_to be_empty
49
+ expect(@error).to be_nil
50
+ expect(@stdout).to be_empty
51
+ expect(@stderr).to be_empty
52
+ expect(@options.error_messages).to include(message)
53
+ end
54
+
55
+ step 'the :attribute attribute should be nil' do |attribute|
56
+ expect(@options.send(attribute.to_sym)).to be_nil
57
+ end
58
+
59
+ step 'the :attribute attribute should be empty' do |attribute|
60
+ expect(@options.send(attribute.to_sym)).to be_empty
61
+ end
62
+
63
+ step 'the :attribute attribute should be ":value"' do |attribute, value|
64
+ expect(@options.send(attribute.to_sym)).to eq(value)
65
+ end
66
+
67
+ step 'the sheets attribute should contain the following Sheets:' do |hash_as_string|
68
+ # rubocop:disable Security/Eval
69
+ expected_sheets = eval(hash_as_string).map { |s| CreateSpreadsheet::Sheet.new(s) }
70
+ # rubocop:enable Security/Eval
71
+ expect(@options.sheets).to match_array(expected_sheets)
72
+ end
73
+
74
+ RSpec.configure do |config|
75
+ config.before(type: :feature) do
76
+ allow(File).to receive(:read).and_call_original
77
+ end
78
+ end
79
+
80
+ step 'a file :path containing:' do |path, content|
81
+ allow(File).to receive(:read).with(path).and_return(content)
82
+ end
83
+
84
+ step 'the file :path does not exist' do |path|
85
+ allow(File).to receive(:read).with(path).and_raise(Errno::ENOENT, "No such file or directory @ rb_sysopen - #{path}")
86
+ end
87
+
88
+ step 'the permissions attribute should contain the following Permissions:' do |hash_as_string|
89
+ # rubocop:disable Security/Eval
90
+ expected_permissions = eval(hash_as_string).map { |p| CreateSpreadsheet::Permission.new(p) }
91
+ # rubocop:enable Security/Eval
92
+ expect(@options.permissions).to match_array(expected_permissions)
93
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Turnip setup
4
+ #
5
+ require 'turnip/rspec'
6
+ Dir[File.join(__dir__, '**/*_steps.rb')].each { |f| require f }
7
+
8
+ RSpec.configure do |config|
9
+ # Enable flags like --only-failures and --next-failure
10
+ config.example_status_persistence_file_path = '.rspec_status'
11
+
12
+ # Disable RSpec exposing methods globally on `Module` and `main`
13
+ config.disable_monkey_patching!
14
+
15
+ config.expect_with :rspec do |c|
16
+ c.syntax = :expect
17
+ end
18
+ end
19
+
20
+ # Setup simplecov
21
+
22
+ require 'simplecov'
23
+ require 'simplecov-lcov'
24
+ require 'json'
25
+
26
+ SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::LcovFormatter]
27
+
28
+ # Return `true` if the environment variable is set to a truthy value
29
+ #
30
+ # @example
31
+ # env_true?('COV_SHOW_UNCOVERED')
32
+ #
33
+ # @param name [String] the name of the environment variable
34
+ # @return [Boolean]
35
+ #
36
+ def env_true?(name)
37
+ value = ENV.fetch(name, '').downcase
38
+ %w[yes on true 1].include?(value)
39
+ end
40
+
41
+ # Return `true` if the environment variable is NOT set to a truthy value
42
+ #
43
+ # @example
44
+ # env_false?('COV_NO_FAIL')
45
+ #
46
+ # @param name [String] the name of the environment variable
47
+ # @return [Boolean]
48
+ #
49
+ def env_false?(name)
50
+ !env_true?(name)
51
+ end
52
+
53
+ # Return `true` if the the test run should fail if the coverage is below the threshold
54
+ #
55
+ # @return [Boolean]
56
+ #
57
+ def fail_on_low_coverage?
58
+ !(RSpec.configuration.dry_run? || env_true?('COV_NO_FAIL'))
59
+ end
60
+
61
+ # Return `true` if the the test run should show the lines not covered by tests
62
+ #
63
+ # @return [Boolean]
64
+ #
65
+ def show_lines_not_covered?
66
+ env_true?('COV_SHOW_UNCOVERED')
67
+ end
68
+
69
+ # Report if the test coverage was below the configured threshold
70
+ #
71
+ # The threshold is configured by setting the `test_coverage_threshold` variable
72
+ # in this file.
73
+ #
74
+ # Example:
75
+ #
76
+ # ```Ruby
77
+ # test_coverage_threshold = 100
78
+ # ```
79
+ #
80
+ # Coverage below the threshold will cause the rspec run to fail unless the
81
+ # `COV_NO_FAIL` environment variable is set to TRUE.
82
+ #
83
+ # ```Shell
84
+ # COV_NO_FAIL=TRUE rspec
85
+ # ```
86
+ #
87
+ # Example of running the tests in an infinite loop writing failures to `fail.txt`:
88
+ #
89
+ # ```Shell
90
+ # while true; do COV_NO_FAIL=TRUE rspec >> fail.txt; done
91
+ # ````
92
+ #
93
+ # The lines missing coverage will be displayed if the `COV_SHOW_UNCOVERED`
94
+ # environment variable is set to TRUE.
95
+ #
96
+ # ```Shell
97
+ # COV_SHOW_UNCOVERED=TRUE rspec
98
+ # ```
99
+ #
100
+ test_coverage_threshold = 100
101
+
102
+ SimpleCov.at_exit do
103
+ SimpleCov.result.format!
104
+ # rubocop:disable Style/StderrPuts
105
+ if SimpleCov.result.covered_percent < test_coverage_threshold
106
+ $stderr.puts
107
+ $stderr.print 'FAIL: ' if fail_on_low_coverage?
108
+ $stderr.puts "RSpec Test coverage fell below #{test_coverage_threshold}%"
109
+
110
+ if show_lines_not_covered?
111
+ $stderr.puts "\nThe following lines were not covered by tests:\n"
112
+ SimpleCov.result.files.each do |source_file| # SimpleCov::SourceFile
113
+ source_file.missed_lines.each do |line| # SimpleCov::SourceFile::Line
114
+ $stderr.puts " .#{source_file.project_filename}:#{line.number}"
115
+ end
116
+ end
117
+ end
118
+
119
+ $stderr.puts
120
+
121
+ exit 1 if fail_on_low_coverage?
122
+ end
123
+ # rubocop:enable Style/StderrPuts
124
+ end
125
+
126
+ SimpleCov.start do
127
+ enable_coverage :branch
128
+ end
129
+
130
+ require 'create_spreadsheet'
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'command_line_boss'
5
+
6
+ # Define a command line interface for creating a spreadsheet
7
+ class CreateSpreadsheetCli < CommandLineBoss
8
+ attr_reader :spreadsheet_name, :sheet_names
9
+
10
+ private
11
+
12
+ def set_defaults
13
+ @spreadsheet_name = nil
14
+ @sheet_names = []
15
+ end
16
+
17
+ def define_sheet_option
18
+ parser.on('--sheet=SHEET_NAME', 'Name of a sheet to create') do |name|
19
+ add_error_message('Sheet names must be unique!') if sheet_names.include?(name)
20
+ sheet_names << name
21
+ end
22
+ end
23
+
24
+ def validate_spreadsheet_name_given
25
+ add_error_message('A spreadsheet name is required') if spreadsheet_name.nil?
26
+ end
27
+
28
+ def validate_at_least_one_sheet_name_given
29
+ add_error_message('At least one sheet name is required') if sheet_names.empty?
30
+ end
31
+
32
+ def parse_arguments
33
+ @spreadsheet_name = args.shift
34
+ end
35
+
36
+ include CommandLineBoss::HelpOption
37
+
38
+ def banner = <<~BANNER
39
+ Create a spreadsheetasdf
40
+
41
+ Usage:
42
+ create_spreadsheet SPREADSHEET_NAME --sheet=SHEET_NAME [--sheet=SHEET_NAME ...]
43
+
44
+ BANNER
45
+ end
46
+
47
+ # Parse the command line arguments
48
+
49
+ options = CreateSpreadsheetCli.new.parse(ARGV)
50
+
51
+ # Report errors
52
+
53
+ if options.failed?
54
+ warn options.error_messages.join("\n")
55
+ exit 1
56
+ end
57
+
58
+ # Do something with the result
59
+
60
+ require 'pp'
61
+
62
+ puts \
63
+ "Creating spreadsheet #{options.spreadsheet_name.pretty_inspect.chomp} " \
64
+ "with sheets #{options.sheet_names.map(&:pretty_inspect).map(&:chomp).join(', ')}"
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CommandLineBoss
4
+ # Add the --help option
5
+ module HelpOption
6
+ private
7
+
8
+ # Define the command line options
9
+ #
10
+ # @return [void]
11
+ #
12
+ # @api private
13
+ #
14
+ def define_options
15
+ add_banner
16
+ add_header
17
+ super
18
+ add_footer
19
+ end
20
+
21
+ # Define the --help option
22
+ #
23
+ # @return [void]
24
+ #
25
+ # @api private
26
+ #
27
+ def define_help_option
28
+ parser.on('-h', '--help', 'Show this message') do
29
+ puts parser.help
30
+ exit
31
+ end
32
+ end
33
+
34
+ # Adds the banner to the parser output
35
+ # @return [void]
36
+ # @api private
37
+ #
38
+ def add_banner
39
+ return unless banner
40
+
41
+ parser.banner = banner
42
+ end
43
+
44
+ # Derived classes should override this method to provide a banner
45
+ # @return [String, nil]
46
+ # @api private
47
+ #
48
+ def banner = nil
49
+
50
+ # Adds the header to the parser output
51
+ # @return [void]
52
+ # @api private
53
+ #
54
+ def add_header
55
+ return unless header
56
+
57
+ parser.separator header
58
+ end
59
+
60
+ # Derived classes should override this method to provide a header
61
+ # @return [String, nil]
62
+ # @api private
63
+ #
64
+ def header = nil
65
+
66
+ # Adds the footer to the parser output
67
+ # @return [void]
68
+ # @api private
69
+ #
70
+ def add_footer
71
+ return unless footer
72
+
73
+ parser.separator footer
74
+ end
75
+
76
+ # Derived classes should override this method to provide a footer
77
+ # @return [String, nil]
78
+ # @api private
79
+ #
80
+ def footer = nil
81
+ end
82
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CommandLineBoss
4
+ # Add --debug and --verbose options and a logger method
5
+ module LoggerOptions
6
+ # true if the --debug option was given
7
+ #
8
+ # @return [Boolean]
9
+ #
10
+ # @api private
11
+ #
12
+ attr_reader :debug
13
+
14
+ # true if the --verbose option was given
15
+ #
16
+ # @return [Boolean]
17
+ #
18
+ # @api private
19
+ #
20
+ attr_reader :verbose
21
+
22
+ # The logger to use to report progress
23
+ #
24
+ # Messages are logged at info and debug levels. The logger returned is one of
25
+ # the following:
26
+ #
27
+ # * A logger that logs to the console at the :info level if verbose mode is enabled
28
+ # * A logger that logs to the console at the :debug level if debug mode is enabled
29
+ # * Otherwise a null logger that does not log anything
30
+ #
31
+ # @example
32
+ # options.logger #=> #<Logger:0x00007f9e3b8b3e08>
33
+ #
34
+ # @return [Logger]
35
+ #
36
+ def logger
37
+ @logger ||=
38
+ if verbose
39
+ verbose_logger
40
+ elsif debug
41
+ debug_logger
42
+ else
43
+ Logger.new(nil, level: Logger::UNKNOWN + 1)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # Define the --verbose option
50
+ #
51
+ # @return [void]
52
+ #
53
+ # @api private
54
+ #
55
+ def define_verbose_option
56
+ parser.on('-v', '--verbose', 'Enable verbose mode (default is off)') do |verbose|
57
+ @verbose = verbose
58
+ end
59
+ end
60
+
61
+ # Define the --debug option
62
+ #
63
+ # @return [void]
64
+ #
65
+ # @api private
66
+ #
67
+ def define_debug_option
68
+ parser.on('-D', '--debug', 'Enable debug mode default is off') do |debug|
69
+ @debug = debug
70
+ end
71
+ end
72
+
73
+ # Ensure that the --debug and --verbose options are not both given
74
+ #
75
+ # @return [void]
76
+ #
77
+ # @api private
78
+ #
79
+ def validate_debug_verbose_option
80
+ add_error_message('Can not give both --debug and --verbose') if debug && verbose
81
+ end
82
+
83
+ # A Logger that logs to the console at the :debug level with a simple formatter
84
+ #
85
+ # @return [Logger]
86
+ #
87
+ # @api private
88
+ #
89
+ def debug_logger
90
+ Logger.new($stdout, level: 'debug', formatter: ->(_severity, _datetime, _progname, msg) { "#{msg}\n" })
91
+ end
92
+
93
+ # A Logger that logs to the console at the :info level with a simple formatter
94
+ #
95
+ # @return [Logger]
96
+ #
97
+ # @api private
98
+ #
99
+ def verbose_logger
100
+ Logger.new($stdout, level: 'info', formatter: ->(_severity, _datetime, _progname, msg) { "#{msg}\n" })
101
+ end
102
+ end
103
+ end