command_line_boss 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +34 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +34 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +340 -0
- data/Rakefile +90 -0
- data/examples/create_spreadsheet/.gitignore +21 -0
- data/examples/create_spreadsheet/.rspec +3 -0
- data/examples/create_spreadsheet/.rubocop.yml +34 -0
- data/examples/create_spreadsheet/Gemfile +16 -0
- data/examples/create_spreadsheet/README.md +314 -0
- data/examples/create_spreadsheet/Rakefile +50 -0
- data/examples/create_spreadsheet/create_spreadsheet.gemspec +39 -0
- data/examples/create_spreadsheet/exe/create-spreadsheet +28 -0
- data/examples/create_spreadsheet/lib/create_spreadsheet/command_line.rb +221 -0
- data/examples/create_spreadsheet/lib/create_spreadsheet.rb +8 -0
- data/examples/create_spreadsheet/spec/create_spreadsheet/command_line.feature +239 -0
- data/examples/create_spreadsheet/spec/create_spreadsheet/command_line_steps.rb +93 -0
- data/examples/create_spreadsheet/spec/spec_helper.rb +130 -0
- data/examples/readme_example/create-spreadsheet +64 -0
- data/lib/command_line_boss/help_option.rb +82 -0
- data/lib/command_line_boss/logger_options.rb +103 -0
- data/lib/command_line_boss/version.rb +6 -0
- data/lib/command_line_boss.rb +246 -0
- metadata +266 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
gem 'command_line_boss', path: File.expand_path(`git rev-parse --show-toplevel`.chomp)
|
6
|
+
|
7
|
+
gem 'create_spreadsheet', path: '.'
|
8
|
+
|
9
|
+
gem 'csv', '~> 3.3'
|
10
|
+
gem 'fuubar', '~> 2.5'
|
11
|
+
gem 'rake', '~> 13.0'
|
12
|
+
gem 'rspec', '~> 3.0'
|
13
|
+
gem 'rubocop', '~> 1.64'
|
14
|
+
gem 'simplecov', '~> 0.22'
|
15
|
+
gem 'simplecov-lcov', '~> 0.8'
|
16
|
+
gem 'turnip', '~> 4.4'
|
@@ -0,0 +1,314 @@
|
|
1
|
+
# Example: create-spreadsheet
|
2
|
+
|
3
|
+
Ideally, with a command line parser class, you want a class that you can simply pass
|
4
|
+
in ARGV to the parser and get the values parsed from the command line using getter
|
5
|
+
methods.
|
6
|
+
|
7
|
+
This example shows step-by-step how to write a class that implements parsing the
|
8
|
+
command line for creating a Google Docs Spreadsheet.
|
9
|
+
|
10
|
+
* [Step 1: Define requirements](#step-1-define-requirements)
|
11
|
+
* [Step 2: Design the command line](#step-2-design-the-command-line)
|
12
|
+
* [Step 3: Implement accessors and set default values for result attributes](#step-3-implement-accessors-and-set-default-values-for-result-attributes)
|
13
|
+
* [Step 4: Write tests](#step-4-write-tests)
|
14
|
+
* [Step 5: Define options](#step-5-define-options)
|
15
|
+
* [Step 6: Define validations](#step-6-define-validations)
|
16
|
+
* [Step 7: Parse remaining command line arguments](#step-7-parse-remaining-command-line-arguments)
|
17
|
+
* [Step 8: Make the --help output look good](#step-8-make-the---help-output-look-good)
|
18
|
+
|
19
|
+
## Step 1: Define requirements
|
20
|
+
|
21
|
+
For this example, write a class that implements parsing the command line that meets
|
22
|
+
the following requirements:
|
23
|
+
|
24
|
+
* Optionally specify the **title** of the spreadsheet. If not specified, use the
|
25
|
+
default title assigned by Google for new spreadsheets (usually "Untitled
|
26
|
+
Spreadsheet")
|
27
|
+
* Optionally specify a list of **sheets** to create and give an optional CSV data
|
28
|
+
file to preload into each sheet. If no sheets are specified, one blank sheet is
|
29
|
+
created by Google with the default title (usually "Sheet 1"). If a data file is not
|
30
|
+
specified for a given sheet, that sheet should be left blank.
|
31
|
+
* Optionally specify a list of **permissions** to add to the spreadsheet. The user
|
32
|
+
must be able to specify the type of the permission (user, group, domain, or
|
33
|
+
anyone), the subject of the permission (depending on the type either an email
|
34
|
+
address, domain name or nothing), and the role being granted (organizer,
|
35
|
+
fileOrganizer, writer, commenter, or reader).
|
36
|
+
* Optionally specify the Google Drive **folder** to create the spreadsheet in. If not
|
37
|
+
given, the spreadsheet will be created in the root drive directory of the user
|
38
|
+
whose credentials are being used to make the request.
|
39
|
+
|
40
|
+
Once the command line is parsed, return this information in the following attributes:
|
41
|
+
|
42
|
+
* **title**: the optional title of the spreadsheet
|
43
|
+
* **sheets**: an array of sheets each having attributes **title** and **data** (read from the given CSV)
|
44
|
+
* **permissions**: an array of permissions each having attributes **type**, **subject**, and **role**
|
45
|
+
* **folder_id**: the id of the Google Drive folder in which to create the spreadsheet
|
46
|
+
|
47
|
+
## Step 2: Design the command line
|
48
|
+
|
49
|
+
Design the command line following the
|
50
|
+
[Google developer documentation style guide for command line syntax](https://developers.google.com/style/code-syntax).
|
51
|
+
|
52
|
+
Document what the `--help` option SHOULD output in the following sections:
|
53
|
+
|
54
|
+
```text
|
55
|
+
BANNER
|
56
|
+
HEADER
|
57
|
+
OPTIONS
|
58
|
+
FOOTER
|
59
|
+
```
|
60
|
+
|
61
|
+
For this example, here is what the --help output should be:
|
62
|
+
|
63
|
+
```text
|
64
|
+
Create a new Google Spreadsheet
|
65
|
+
|
66
|
+
Usage:
|
67
|
+
|
68
|
+
create_spreadsheet [SPREADSHEET_TITLE] \
|
69
|
+
[--sheet=TITLE [--data=DATA_FILE]]... \
|
70
|
+
[--folder=FOLDER_ID] \
|
71
|
+
[--permission=PERMISSION_SPEC]...
|
72
|
+
|
73
|
+
Options:
|
74
|
+
--sheet=TITLE Title of a sheet to create
|
75
|
+
--data=DATA_FILE Data file for the last sheet
|
76
|
+
--folder=FOLDER_ID Create the spreadsheet in the given Google Drive folder
|
77
|
+
--permission=PERMISSION_SPEC Set permissions on the spreadsheet
|
78
|
+
|
79
|
+
DATA_FILE := A file containing data in CSV format
|
80
|
+
PERMISSION_SPEC := {user:EMAIL:ROLE | group:EMAIL:ROLE | domain:DOMAIN:ROLE | anyone:ROLE}
|
81
|
+
ROLE := {organizer | fileOrganizer | writer | commenter | reader}
|
82
|
+
```
|
83
|
+
|
84
|
+
## Step 3: Implement accessors and set default values for result attributes
|
85
|
+
|
86
|
+
In the requirements, there were 4 public attributes defined for this command line
|
87
|
+
parser: title, sheets, permissions, and folder_id. Use the private `set_defaults`
|
88
|
+
method to set the default values for each attribute.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
# Parse the command line for creating a Google Spreadsheet
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# options = CreateSpreadsheetCLI.new.call(ARGV)
|
95
|
+
#
|
96
|
+
# @!attribute title [String, nil] the title to give the new spreadsheet or nil for the default
|
97
|
+
# @!attribute sheets [Array<Sheet>] the sheets to create including title and data for each
|
98
|
+
# @!attribute permissions [Array<Permission>] the permissions to add to the spreadsheet
|
99
|
+
# @!attribute folder_id [String] the id of the Google Drive folder in which to create the spreadsheet
|
100
|
+
#
|
101
|
+
# @api public
|
102
|
+
#
|
103
|
+
class CreateSpreadsheetCli < CommandLineBoss
|
104
|
+
attr_reader :title, :sheets, :folder, :permissions
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def set_defaults
|
109
|
+
@title = nil
|
110
|
+
@sheets = []
|
111
|
+
@permissions = []
|
112
|
+
@folder_id = nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
In this case, it would be advantageous to create a few supporting classes and
|
118
|
+
constants to return sheets and permissions. Add the following code to the
|
119
|
+
CreateSpreadsheetCli class.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
# Sheets specified at the command line
|
123
|
+
#
|
124
|
+
# @!attribute title [String, nil] the title of the sheet to create or nil for the default title
|
125
|
+
# @!attribute data [Array<Array<Object>>, nil] the data to write to the sheet or nil
|
126
|
+
#
|
127
|
+
# @api public
|
128
|
+
Sheet = Struct.new(:title, :data)
|
129
|
+
|
130
|
+
# Permissions specified at the command line
|
131
|
+
#
|
132
|
+
# @!attribute type [String] must be one of VALID_PERM_TYPES
|
133
|
+
# @!attribute subject [String, nil] the name of the subject the permission is given to
|
134
|
+
#
|
135
|
+
# * If the type is 'user' or 'group', must be a valid email address
|
136
|
+
# * If type is 'domain', must be a valid domain name
|
137
|
+
# * If type is anyone, must be nil
|
138
|
+
#
|
139
|
+
# @!attribute role [String] myst be one of VALID_PERM_ROLES
|
140
|
+
Permission = Struct.new(:type, :subject, :role)
|
141
|
+
|
142
|
+
VALID_PERM_TYPES = %w[user group domain anyone].freeze
|
143
|
+
VALID_PERM_ROLES = %w[organizer fileOrganizer writer commenter reader].freeze
|
144
|
+
```
|
145
|
+
|
146
|
+
## Step 4: Write tests
|
147
|
+
|
148
|
+
Write tests in your favorite testing framework that assert various permutations of
|
149
|
+
command line arguments result either in:
|
150
|
+
|
151
|
+
* The expected attrbiute values, or
|
152
|
+
* The expected exception was raised
|
153
|
+
|
154
|
+
Tests for this example can be found in spec/create_spreadsheet.feature`
|
155
|
+
Tests for this interface might include:
|
156
|
+
|
157
|
+
* Nothing is given on the command line
|
158
|
+
* A spreadsheet title is given
|
159
|
+
* A sheet is defined with a title
|
160
|
+
* A sheet is defined with a title and data
|
161
|
+
* Multiple sheets are defined both with data
|
162
|
+
* Multiple sheets are defined only one with data
|
163
|
+
* A user permission is given
|
164
|
+
* A group permission is given
|
165
|
+
* A domain permission is given
|
166
|
+
* An anyone permission is given
|
167
|
+
* Multiple permissions are given
|
168
|
+
* A folder is given
|
169
|
+
* A sheet given without a name
|
170
|
+
* A permission is given without a permission spec
|
171
|
+
* A invalid permission is given
|
172
|
+
* The permission spec has an invalid type
|
173
|
+
* A permission spec has an invalid role
|
174
|
+
* A subject is given for an anyone permission
|
175
|
+
* A subject is not given for a user permission
|
176
|
+
* Data is given without a path
|
177
|
+
* Data is given with an non-existant path
|
178
|
+
* The folder option is given twice
|
179
|
+
* A same sheet name is given twice
|
180
|
+
* Data is given twice for the same sheet
|
181
|
+
|
182
|
+
## Step 5: Define options
|
183
|
+
|
184
|
+
Define private methods whose name is define_*_option.
|
185
|
+
|
186
|
+
Methods MUST be private or they won't be called.
|
187
|
+
|
188
|
+
Add any errors to the `error_messages` array.
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
private
|
192
|
+
|
193
|
+
def define_sheet_title_option
|
194
|
+
parser.on('--sheet-title=TITLE', 'Title of a sheet to create') do |title|
|
195
|
+
sheets << Sheet.new(title:, data: nil)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def define_sheet_data_option
|
200
|
+
parser.on('--sheet-data=DATA_FILE', 'Data file for the last sheet') do |data_file|
|
201
|
+
sheets << Sheet.new(title: nil, data: nil) if sheets.empty?
|
202
|
+
if sheets.last.data
|
203
|
+
error_messages << 'Only one --sheet-data option is allowed per --sheet-title'
|
204
|
+
else
|
205
|
+
sheets.last.data = CSV.read(data_file)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def define_folder_option
|
211
|
+
parser.on('--folder=FOLDER_ID', 'Create the spreadsheet to the given folder') do |folder_id|
|
212
|
+
if @folder_id
|
213
|
+
error_messages << 'Only one --folder option is allowed'
|
214
|
+
else
|
215
|
+
@folder_id = folder_id
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
PERMISSION_SPEC_REGEXP = /
|
221
|
+
^
|
222
|
+
(?<type>[^:]+)
|
223
|
+
(?:
|
224
|
+
:(?<subject>[^:]+)
|
225
|
+
)?
|
226
|
+
:(?<role>[^:]+)
|
227
|
+
$
|
228
|
+
/x
|
229
|
+
|
230
|
+
def define_permission_option
|
231
|
+
parser.on('--permission=PERMISSION_SPEC', 'Set permissions on the spreadsheet') do |permission_spec|
|
232
|
+
match = permission_spec.match(PERMISSION_SPEC_REGEXP)
|
233
|
+
unless match
|
234
|
+
error_messages << "Invalid permission spec: #{permission_spec}"
|
235
|
+
next
|
236
|
+
end
|
237
|
+
permissions << Permission.new(
|
238
|
+
permission_spec:, type: match[:type], subject: match[:subject], role: match[:role]
|
239
|
+
)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
## Step 6: Define validations
|
245
|
+
|
246
|
+
Define private methods whose name is validate_*.
|
247
|
+
|
248
|
+
Methods MUST be private or they won't be called.
|
249
|
+
|
250
|
+
Add any errors to the `error_messages` array.
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
def validate_permission_types
|
254
|
+
permissions.each do |permission|
|
255
|
+
unless VALID_PERMISSION_TYPES.include?(permission.type)
|
256
|
+
error_messages << "Invalid permission type: #{permission.type}"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def validate_permission_roles
|
262
|
+
permissions.each do |permission|
|
263
|
+
unless VALID_PERMISSION_ROLES.include?(permission.role)
|
264
|
+
error_messages << "Invalid permission role: #{permission.role}"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def validate_permission_subjects
|
270
|
+
permissions.each do |permission|
|
271
|
+
if permission.type == 'anyone' && permission.subject
|
272
|
+
error_messages << "Permission subject for type 'anyone' should be blank in #{permission.permission_spec}"
|
273
|
+
end
|
274
|
+
if permission.type != 'anyone' && !permission.subject
|
275
|
+
error_messages << "Permission subject missing in #{permission.permission_spec}"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
```
|
280
|
+
|
281
|
+
## Step 7: Parse remaining command line arguments
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
def parse_arguments
|
285
|
+
@spreadsheet_title = args.shift
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
## Step 8: Make the --help output look good
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
def banner = <<~TEXT
|
293
|
+
Create a new Google Spreadsheet
|
294
|
+
TEXT
|
295
|
+
|
296
|
+
# Usage text for the command line help
|
297
|
+
# @api private
|
298
|
+
def usage = <<~TEXT
|
299
|
+
Usage:
|
300
|
+
|
301
|
+
create_spreadsheet SPREADSHEET_TITLE \
|
302
|
+
[--sheet-title=TITLE [--sheet-data=DATA_FILE]]... \
|
303
|
+
[--folder=FOLDER_ID }] \
|
304
|
+
[--permission=PERMISSION_SPEC]...
|
305
|
+
TEXT
|
306
|
+
|
307
|
+
# Footer text for the command line help
|
308
|
+
# @api private
|
309
|
+
def footer = <<~TEXT
|
310
|
+
PERMISSION_SPEC := {user:EMAIL:ROLE | group:EMAIL:ROLE | domain:DOMAIN:ROLE | anyone:ROLE}
|
311
|
+
ROLE := {organizer | fileOrganizer | writer | commenter | reader}
|
312
|
+
DATA_FILE := A file containing data in CSV format
|
313
|
+
TEXT
|
314
|
+
```
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
task default: %i[spec rubocop build]
|
4
|
+
|
5
|
+
require 'rake/clean'
|
6
|
+
|
7
|
+
# RSpec
|
8
|
+
|
9
|
+
require 'rake'
|
10
|
+
require 'rspec/core/rake_task'
|
11
|
+
|
12
|
+
RSpec::Core::RakeTask.new do |t|
|
13
|
+
t.pattern = './spec/**/*{_spec.rb,.feature}'
|
14
|
+
end
|
15
|
+
|
16
|
+
CLEAN << 'coverage'
|
17
|
+
CLEAN << 'test'
|
18
|
+
CLEAN << '.rspec_status'
|
19
|
+
CLEAN << 'rspec-report.xml'
|
20
|
+
|
21
|
+
# Bundler Gem Build
|
22
|
+
|
23
|
+
require 'bundler'
|
24
|
+
require 'bundler/gem_tasks'
|
25
|
+
|
26
|
+
begin
|
27
|
+
Bundler.setup(:default, :development)
|
28
|
+
rescue Bundler::BundlerError => e
|
29
|
+
warn e.message
|
30
|
+
warn 'Run `bundle install` to install missing gems'
|
31
|
+
exit e.status_code
|
32
|
+
end
|
33
|
+
|
34
|
+
CLEAN << 'pkg'
|
35
|
+
CLOBBER << 'Gemfile.lock'
|
36
|
+
|
37
|
+
# Rubocop
|
38
|
+
|
39
|
+
require 'rubocop/rake_task'
|
40
|
+
|
41
|
+
RuboCop::RakeTask.new do |t|
|
42
|
+
t.options = %w[
|
43
|
+
--format fuubar
|
44
|
+
--format json --out rubocop-report.json
|
45
|
+
--display-cop-names
|
46
|
+
--config .rubocop.yml
|
47
|
+
]
|
48
|
+
end
|
49
|
+
|
50
|
+
CLEAN << 'rubocop-report.json'
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'create_spreadsheet'
|
5
|
+
spec.version = '1.0.0'
|
6
|
+
spec.authors = ['James Couball']
|
7
|
+
spec.email = ['jcouball@yahoo.com']
|
8
|
+
|
9
|
+
spec.summary = 'A command line utility to create a Google Spreadsheet'
|
10
|
+
spec.description = ''
|
11
|
+
spec.license = 'MIT'
|
12
|
+
spec.required_ruby_version = '>= 3.1.0'
|
13
|
+
|
14
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
15
|
+
|
16
|
+
spec.homepage = 'https://github.com/main-branch/create_spreadsheet'
|
17
|
+
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
19
|
+
spec.metadata['source_code_uri'] = 'https://github.com/main-branch/create_spreadsheet'
|
20
|
+
spec.metadata['changelog_uri'] = "https://rubydoc.info/gems/#{spec.name}/#{spec.version}/file/CHANGELOG.md"
|
21
|
+
spec.metadata['documentation_uri'] = "https://rubydoc.info/gems/#{spec.name}/#{spec.version}"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
gemspec = File.basename(__FILE__)
|
26
|
+
spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
|
27
|
+
ls.readlines("\x0", chomp: true).reject do |f|
|
28
|
+
(f == gemspec) ||
|
29
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
spec.bindir = 'exe'
|
33
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
34
|
+
spec.require_paths = ['lib']
|
35
|
+
|
36
|
+
# For more information and examples about making a new gem, check out our
|
37
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
38
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
39
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'create_spreadsheet'
|
5
|
+
require 'csv'
|
6
|
+
require 'pp'
|
7
|
+
|
8
|
+
options = CreateSpreadsheet::CommandLine.new.parse(ARGV)
|
9
|
+
|
10
|
+
if options.failed?
|
11
|
+
warn "Parsing failed:\n#{options.error_messages.join("\n")}"
|
12
|
+
exit 1
|
13
|
+
end
|
14
|
+
|
15
|
+
# options =
|
16
|
+
# begin
|
17
|
+
# CreateSpreadsheet::CommandLine.new.parse(ARGV)
|
18
|
+
# rescue CommandLineBoss::Error => e
|
19
|
+
# warn "ERROR: #{e.parser.error_messages.join("\nERROR: ")}"
|
20
|
+
# exit 1
|
21
|
+
# end
|
22
|
+
|
23
|
+
puts <<~OPTIONS
|
24
|
+
Creating spreadsheet with title: #{options.title.pretty_inspect}
|
25
|
+
Creating sheets: #{options.sheets.pretty_inspect}
|
26
|
+
In folder: #{options.folder_id.pretty_inspect}
|
27
|
+
Adding permissions: #{options.permissions.pretty_inspect}
|
28
|
+
OPTIONS
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'command_line_boss'
|
4
|
+
require 'csv'
|
5
|
+
|
6
|
+
module CreateSpreadsheet
|
7
|
+
# Sheet`s specified at the command line
|
8
|
+
#
|
9
|
+
# @!attribute title [String, nil] the title of the sheet to create or nil for the default title
|
10
|
+
# @!attribute data [Array<Array<Object>>, nil] the data to write to the sheet or nil
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
Sheet = Struct.new(:title, :data, keyword_init: true)
|
14
|
+
|
15
|
+
# Permissions specified at the command line
|
16
|
+
#
|
17
|
+
# @!attribute permission_spec [String] the unparsed permission spec as given at the command line
|
18
|
+
# @!attribute type [String] must be one of VALID_PERM_TYPES
|
19
|
+
# @!attribute subject [String, nil] the name of the subject the permission is given to
|
20
|
+
#
|
21
|
+
# * If the type is 'user' or 'group', must be a valid email address
|
22
|
+
# * If type is 'domain', must be a valid domain name
|
23
|
+
# * If type is anyone, must be nil
|
24
|
+
#
|
25
|
+
# @!attribute role [String] myst be one of VALID_PERM_ROLES
|
26
|
+
Permission = Struct.new(:permission_spec, :type, :subject, :role, keyword_init: true)
|
27
|
+
|
28
|
+
# The list of valid permission types the user can give
|
29
|
+
# @return [Array<String>]
|
30
|
+
# @api private
|
31
|
+
VALID_PERM_TYPES = %w[user group domain anyone].freeze
|
32
|
+
|
33
|
+
# The list of valid permission roles the user can give
|
34
|
+
# @return [Array<String>]
|
35
|
+
# @api private
|
36
|
+
VALID_PERM_ROLES = %w[organizer fileOrganizer writer commenter reader].freeze
|
37
|
+
|
38
|
+
# The regular expression for parsing a permission spec
|
39
|
+
# @return [Regexp]
|
40
|
+
PERMISSION_SPEC_REGEXP = /
|
41
|
+
^
|
42
|
+
(?<type>[^:]+)
|
43
|
+
(?:
|
44
|
+
:(?<subject>[^:]+)
|
45
|
+
)?
|
46
|
+
:(?<role>[^:]+)
|
47
|
+
$
|
48
|
+
/x
|
49
|
+
|
50
|
+
# A command line interface for creating spreadsheets
|
51
|
+
#
|
52
|
+
# @!attribute [r] title
|
53
|
+
# @return [String, nil] The title of the spreadsheet to create or nil for the default title
|
54
|
+
#
|
55
|
+
# @!attribute [r] sheets
|
56
|
+
# @return [Array<Sheet>] The sheets to create in the spreadsheet
|
57
|
+
#
|
58
|
+
# @!attribute [r] permissions
|
59
|
+
# @return [Array<Permission>] The list of permissions to add to the spreadsheet
|
60
|
+
#
|
61
|
+
# @!attribute [r] folder_id
|
62
|
+
# @return [String, nil] The ID of the folder to move the spreadsheet to
|
63
|
+
#
|
64
|
+
# @example Create a spreadsheet named "My Spreadsheet" with a default sheet named "Sheet1"
|
65
|
+
# ARGV #=> ["My Spreadsheet"]
|
66
|
+
# options = CommandLineParser.new.call(ARGV)
|
67
|
+
# options.spreadsheet_title #=> "My Spreadsheet"
|
68
|
+
#
|
69
|
+
# @api pubic
|
70
|
+
#
|
71
|
+
class CommandLine < CommandLineBoss
|
72
|
+
attr_reader :title, :sheets, :permissions, :folder_id
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Set the attribute default values
|
77
|
+
# @return [void]
|
78
|
+
# @api private
|
79
|
+
def set_defaults
|
80
|
+
@title = nil
|
81
|
+
@sheets = []
|
82
|
+
@permissions = []
|
83
|
+
@folder_id = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# Remove the title from the remaining arguments
|
87
|
+
# @return [void]
|
88
|
+
# @api private
|
89
|
+
def parse_arguments
|
90
|
+
@title = @args.shift
|
91
|
+
end
|
92
|
+
|
93
|
+
# Define the --sheet option
|
94
|
+
# @return [void]
|
95
|
+
# @api private
|
96
|
+
def define_sheet_option
|
97
|
+
parser.on('--sheet=TITLE', 'Title of a sheet to create') do |title|
|
98
|
+
if sheets.any? { |sheet| sheet.title.downcase == title.downcase }
|
99
|
+
add_error_message("The sheet #{title} was given more than once")
|
100
|
+
end
|
101
|
+
|
102
|
+
sheets << Sheet.new(title:, data: nil)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Read the csv data file
|
107
|
+
# @param data_file [String] the name of the data file
|
108
|
+
# @return [Array<Array<Object>>, nil] the data in the file or nil if the file is not found
|
109
|
+
# @api private
|
110
|
+
def read_data_file(data_file)
|
111
|
+
CSV.parse(File.read(data_file))
|
112
|
+
rescue Errno::ENOENT
|
113
|
+
add_error_message "Data file not found: #{data_file}"
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
# Define the --data option
|
118
|
+
# @return [void]
|
119
|
+
# @api private
|
120
|
+
def define_data_option
|
121
|
+
parser.on('--data=DATA_FILE', 'Data file for the last named sheet') do |data_file|
|
122
|
+
sheets << Sheet.new(title: nil, data: nil) if sheets.empty?
|
123
|
+
if sheets.last.data
|
124
|
+
add_error_message 'Only one data file is allowed per sheet'
|
125
|
+
else
|
126
|
+
sheets.last.data = read_data_file(data_file)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Define the --permission option
|
132
|
+
# @return [void]
|
133
|
+
# @api private
|
134
|
+
def define_permission_option
|
135
|
+
parser.on('--permission=PERMISSION_SPEC', 'Set permissions on the spreadsheet') do |permission_spec|
|
136
|
+
match = permission_spec.match(PERMISSION_SPEC_REGEXP)
|
137
|
+
unless match
|
138
|
+
add_error_message "Invalid permission: #{permission_spec}"
|
139
|
+
next
|
140
|
+
end
|
141
|
+
permissions << Permission.new(
|
142
|
+
permission_spec:, type: match[:type], subject: match[:subject], role: match[:role]
|
143
|
+
)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Define the --folder option
|
148
|
+
# @return [void]
|
149
|
+
# @api private
|
150
|
+
def define_folder_option
|
151
|
+
parser.on('--folder=FOLDER_ID', 'Create the spreadsheet to the given folder') do |folder_id|
|
152
|
+
if @folder_id
|
153
|
+
add_error_message 'Only one --folder option is allowed'
|
154
|
+
else
|
155
|
+
@folder_id = folder_id
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Validate the permission role value
|
161
|
+
# @return [void]
|
162
|
+
# @api private
|
163
|
+
def validate_permission_role
|
164
|
+
permissions.each do |p|
|
165
|
+
add_error_message "Invalid permission role: #{p.role}" unless VALID_PERM_ROLES.include?(p.role)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Validate that no permissions of type 'anyone' has a subject
|
170
|
+
# @return [void]
|
171
|
+
# @api private
|
172
|
+
def validate_permission_anyone_subject
|
173
|
+
permissions.each do |p|
|
174
|
+
add_error_message 'An anyone permission must not have a subject' if p.type == 'anyone' && p.subject
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Validate that all permissions that are not type 'anyone' have a subject
|
179
|
+
# @return [void]
|
180
|
+
# @api private
|
181
|
+
def validate_permission_other_subject
|
182
|
+
permissions.each do |p|
|
183
|
+
add_error_message "A #{p.type} permission must have a subject" if p.type != 'anyone' && !p.subject
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Validate that all permissions have a valid type
|
188
|
+
# @return [void]
|
189
|
+
# @api private
|
190
|
+
def validate_permission_type
|
191
|
+
permissions.each do |p|
|
192
|
+
add_error_message "Invalid permission type: #{p.type}" unless VALID_PERM_TYPES.include?(p.type)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
include CommandLineBoss::HelpOption
|
197
|
+
|
198
|
+
# Banner shown at the top of the help message
|
199
|
+
# @return [String]
|
200
|
+
# @api private
|
201
|
+
def banner = <<~BANNER
|
202
|
+
Create a new Google Spreadsheet'
|
203
|
+
|
204
|
+
Usage:
|
205
|
+
|
206
|
+
create_spreadsheet [SPREADSHEET_TITLE] \\
|
207
|
+
[--sheet=TITLE [--data=DATA_FILE]]... \\
|
208
|
+
[--folder=FOLDER_ID] \\
|
209
|
+
[--permission=PERMISSION_SPEC]...
|
210
|
+
BANNER
|
211
|
+
|
212
|
+
# Footer shown at the bottom of the help message
|
213
|
+
# @return [String]
|
214
|
+
# @api private
|
215
|
+
def footer = <<~FOOTER
|
216
|
+
DATA_FILE := A file containing data in CSV format
|
217
|
+
PERMISSION_SPEC := {user:EMAIL:ROLE | group:EMAIL:ROLE | domain:DOMAIN:ROLE | anyone:ROLE}
|
218
|
+
ROLE := {organizer | fileOrganizer | writer | commenter | reader}
|
219
|
+
FOOTER
|
220
|
+
end
|
221
|
+
end
|