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.
- 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
|