cuprum-cli 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/CHANGELOG.md +34 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE +21 -0
- data/README.md +163 -0
- data/lib/cuprum/cli/argument.rb +172 -0
- data/lib/cuprum/cli/arguments/class_methods.rb +283 -0
- data/lib/cuprum/cli/arguments.rb +16 -0
- data/lib/cuprum/cli/coercion.rb +131 -0
- data/lib/cuprum/cli/command.rb +102 -0
- data/lib/cuprum/cli/commands/ci/report.rb +121 -0
- data/lib/cuprum/cli/commands/ci/rspec_command.rb +108 -0
- data/lib/cuprum/cli/commands/ci/rspec_each_command.rb +185 -0
- data/lib/cuprum/cli/commands/ci.rb +12 -0
- data/lib/cuprum/cli/commands/echo_command.rb +76 -0
- data/lib/cuprum/cli/commands/file/generate_file.rb +141 -0
- data/lib/cuprum/cli/commands/file/new_command.rb +86 -0
- data/lib/cuprum/cli/commands/file/render_erb.rb +88 -0
- data/lib/cuprum/cli/commands/file/resolve_template.rb +136 -0
- data/lib/cuprum/cli/commands/file/templates/rspec.rb.erb +14 -0
- data/lib/cuprum/cli/commands/file/templates/ruby.rb.erb +29 -0
- data/lib/cuprum/cli/commands/file/templates.rb +71 -0
- data/lib/cuprum/cli/commands/file.rb +14 -0
- data/lib/cuprum/cli/commands.rb +12 -0
- data/lib/cuprum/cli/dependencies/file_system/mock.rb +297 -0
- data/lib/cuprum/cli/dependencies/file_system.rb +247 -0
- data/lib/cuprum/cli/dependencies/standard_io/helpers.rb +138 -0
- data/lib/cuprum/cli/dependencies/standard_io/mock.rb +85 -0
- data/lib/cuprum/cli/dependencies/standard_io.rb +110 -0
- data/lib/cuprum/cli/dependencies/system_command/mock.rb +57 -0
- data/lib/cuprum/cli/dependencies/system_command.rb +147 -0
- data/lib/cuprum/cli/dependencies.rb +25 -0
- data/lib/cuprum/cli/errors/files/file_not_writeable.rb +42 -0
- data/lib/cuprum/cli/errors/files/missing_parameter.rb +71 -0
- data/lib/cuprum/cli/errors/files/missing_template.rb +36 -0
- data/lib/cuprum/cli/errors/files/template_error.rb +37 -0
- data/lib/cuprum/cli/errors/files/template_not_resolved.rb +54 -0
- data/lib/cuprum/cli/errors/files.rb +19 -0
- data/lib/cuprum/cli/errors/system_command_failure.rb +44 -0
- data/lib/cuprum/cli/errors.rb +11 -0
- data/lib/cuprum/cli/integrations/thor/arguments_parser.rb +99 -0
- data/lib/cuprum/cli/integrations/thor/registry.rb +42 -0
- data/lib/cuprum/cli/integrations/thor/task.rb +211 -0
- data/lib/cuprum/cli/integrations/thor.rb +14 -0
- data/lib/cuprum/cli/integrations.rb +8 -0
- data/lib/cuprum/cli/metadata.rb +215 -0
- data/lib/cuprum/cli/option.rb +165 -0
- data/lib/cuprum/cli/options/class_methods.rb +232 -0
- data/lib/cuprum/cli/options/quiet.rb +32 -0
- data/lib/cuprum/cli/options/verbose.rb +32 -0
- data/lib/cuprum/cli/options.rb +18 -0
- data/lib/cuprum/cli/registry.rb +141 -0
- data/lib/cuprum/cli/rspec/deferred/arguments_examples.rb +203 -0
- data/lib/cuprum/cli/rspec/deferred/ci/report_examples.rb +450 -0
- data/lib/cuprum/cli/rspec/deferred/ci.rb +8 -0
- data/lib/cuprum/cli/rspec/deferred/dependencies/file_system_examples.rb +1469 -0
- data/lib/cuprum/cli/rspec/deferred/dependencies.rb +8 -0
- data/lib/cuprum/cli/rspec/deferred/metadata_examples.rb +856 -0
- data/lib/cuprum/cli/rspec/deferred/options_examples.rb +234 -0
- data/lib/cuprum/cli/rspec/deferred/registry_examples.rb +451 -0
- data/lib/cuprum/cli/rspec/deferred.rb +8 -0
- data/lib/cuprum/cli/rspec.rb +8 -0
- data/lib/cuprum/cli/version.rb +59 -0
- data/lib/cuprum/cli.rb +47 -0
- metadata +173 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/cli/dependencies/file_system'
|
|
4
|
+
|
|
5
|
+
module Cuprum::Cli::Dependencies
|
|
6
|
+
# Mock implementation of FileSystem for testing purposes.
|
|
7
|
+
class FileSystem::Mock < Cuprum::Cli::Dependencies::FileSystem # rubocop:disable Metrics/ClassLength
|
|
8
|
+
SINGLE_GLOB_PATTERN = /\*+/
|
|
9
|
+
private_constant :SINGLE_GLOB_PATTERN
|
|
10
|
+
|
|
11
|
+
# Exception raised when trying to read from or write to a non-mocked path.
|
|
12
|
+
class InvalidPathError < StandardError; end
|
|
13
|
+
|
|
14
|
+
# Utility class used to simulate tempfile behavior.
|
|
15
|
+
class MockTempfile < SimpleDelegator
|
|
16
|
+
# @param path [String] the qualified path to the tempfile.
|
|
17
|
+
def initialize(path)
|
|
18
|
+
super(StringIO.new)
|
|
19
|
+
|
|
20
|
+
@path = path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [String] the qualified path to the tempfile.
|
|
24
|
+
attr_reader :path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param files [Hash{String => Hash, IO}] the mocked directories and files.
|
|
28
|
+
# Must be a Hash with String keys representing file path segments; Hash
|
|
29
|
+
# values represent directories, while IO values represent files.
|
|
30
|
+
# @param root_path [String] the path to the root directory. Defaults to the
|
|
31
|
+
# value of `Dir.pwd`.
|
|
32
|
+
def initialize(files: {}, root_path: Dir.pwd)
|
|
33
|
+
super(root_path:)
|
|
34
|
+
|
|
35
|
+
@files = files
|
|
36
|
+
@tempfiles = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Hash{String => Hash, IO}] the mocked directories and files.
|
|
40
|
+
attr_reader :files
|
|
41
|
+
|
|
42
|
+
# @return [Array<String>] the contents of each generated tempfile.
|
|
43
|
+
attr_reader :tempfiles
|
|
44
|
+
|
|
45
|
+
# (see Cuprum::Cli::Dependencies::FileSystem#create_directory)
|
|
46
|
+
def create_directory(path, recursive: false) # rubocop:disable Metrics/MethodLength
|
|
47
|
+
tools.assertions.validate_name(path, as: 'path')
|
|
48
|
+
|
|
49
|
+
*dir_names, dir_name = split_path(resolve_path(path))
|
|
50
|
+
|
|
51
|
+
directory = write_directory(
|
|
52
|
+
*dir_names,
|
|
53
|
+
action: 'create directory',
|
|
54
|
+
file_or_path: path,
|
|
55
|
+
recursive:
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if io_stream?(directory[dir_name])
|
|
59
|
+
raise DirectoryIsAFileError,
|
|
60
|
+
"unable to create directory #{path} - directory is a file"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
directory[dir_name] = {}
|
|
64
|
+
|
|
65
|
+
path
|
|
66
|
+
end
|
|
67
|
+
alias make_directory create_directory
|
|
68
|
+
|
|
69
|
+
# (see Cuprum::Cli::Dependencies::FileSystem#directory?)
|
|
70
|
+
def directory?(path)
|
|
71
|
+
tools.assertions.validate_name(path, as: 'path')
|
|
72
|
+
|
|
73
|
+
path = resolve_path(path)
|
|
74
|
+
mock = resolve_mock(path)
|
|
75
|
+
|
|
76
|
+
mock.is_a?(Hash)
|
|
77
|
+
end
|
|
78
|
+
alias directory_exists? directory?
|
|
79
|
+
|
|
80
|
+
# (see Cuprum::Cli::Dependencies::FileSystem#each_file)
|
|
81
|
+
def each_file(pattern, &)
|
|
82
|
+
return enum_for(:each_file, pattern) unless block_given?
|
|
83
|
+
|
|
84
|
+
flattened_files.each do |file_path|
|
|
85
|
+
next unless matches_pattern?(file_path:, pattern:)
|
|
86
|
+
|
|
87
|
+
yield File.join(root_path, file_path)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# (see Cuprum::Cli::Dependencies::FileSystem#directory?)
|
|
94
|
+
def file?(path)
|
|
95
|
+
tools.assertions.validate_name(path, as: 'path')
|
|
96
|
+
|
|
97
|
+
path = resolve_path(path)
|
|
98
|
+
mock = resolve_mock(path)
|
|
99
|
+
|
|
100
|
+
io_stream?(mock)
|
|
101
|
+
end
|
|
102
|
+
alias file_exists? file?
|
|
103
|
+
|
|
104
|
+
# (see Cuprum::Cli::Dependencies::FileSystem#read_file)
|
|
105
|
+
def read_file(file_or_path) # rubocop:disable Metrics/MethodLength
|
|
106
|
+
validate_file(file_or_path, as: 'file')
|
|
107
|
+
|
|
108
|
+
return file_or_path.read if io_stream?(file_or_path)
|
|
109
|
+
|
|
110
|
+
path = resolve_path(file_or_path)
|
|
111
|
+
|
|
112
|
+
unless path.start_with?(root_path)
|
|
113
|
+
raise FileNotFoundError,
|
|
114
|
+
"unable to read file #{file_or_path} - file not found"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
mock = resolve_mock(path)
|
|
118
|
+
|
|
119
|
+
return mock.tap(&:rewind).read if io_stream?(mock)
|
|
120
|
+
|
|
121
|
+
if mock
|
|
122
|
+
raise FileIsADirectoryError,
|
|
123
|
+
"unable to read file #{file_or_path} - file is a directory"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
raise FileNotFoundError,
|
|
127
|
+
"unable to read file #{file_or_path} - file not found"
|
|
128
|
+
end
|
|
129
|
+
alias read read_file
|
|
130
|
+
|
|
131
|
+
# (see Cuprum::Cli::Dependencies::FileSystem#with_tempfile)
|
|
132
|
+
def with_tempfile(&block)
|
|
133
|
+
file_name = SecureRandom.uuid
|
|
134
|
+
file_path = File.join(root_path, 'tempfiles', file_name)
|
|
135
|
+
|
|
136
|
+
tempfile = MockTempfile.new(file_path)
|
|
137
|
+
|
|
138
|
+
(files['tempfiles'] ||= {})[file_name] = tempfile
|
|
139
|
+
|
|
140
|
+
block.call(tempfile).tap do
|
|
141
|
+
tempfiles << read_file(tempfile.tap(&:rewind))
|
|
142
|
+
end
|
|
143
|
+
ensure
|
|
144
|
+
files['tempfiles'].delete(file_name)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# (see Cuprum::Cli::Dependencies::FileSystem#write_file)
|
|
148
|
+
def write_file(file_or_path, data) # rubocop:disable Metrics/MethodLength
|
|
149
|
+
validate_file(file_or_path, as: 'file')
|
|
150
|
+
|
|
151
|
+
return file_or_path.write(data) if io_stream?(file_or_path)
|
|
152
|
+
|
|
153
|
+
path = resolve_path(file_or_path)
|
|
154
|
+
|
|
155
|
+
unless path.start_with?(root_path)
|
|
156
|
+
raise DirectoryNotFoundError,
|
|
157
|
+
"unable to write file #{file_or_path} - directory not found"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
mock = resolve_mock(path)
|
|
161
|
+
|
|
162
|
+
if mock.is_a?(MockTempfile)
|
|
163
|
+
mock.write(data)
|
|
164
|
+
mock.rewind
|
|
165
|
+
|
|
166
|
+
return
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
if mock.nil? || io_stream?(mock)
|
|
170
|
+
return write_mock_file(path, data, file_or_path)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
raise FileIsADirectoryError,
|
|
174
|
+
"unable to write file #{file_or_path} - file is a directory"
|
|
175
|
+
end
|
|
176
|
+
alias write write_file
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def flatten_files(files:, flat: [], path: '')
|
|
181
|
+
files.each do |name, value|
|
|
182
|
+
qualified_path = path.empty? ? name : File.join(path, name)
|
|
183
|
+
|
|
184
|
+
next flat << qualified_path unless value.is_a?(Hash)
|
|
185
|
+
|
|
186
|
+
flatten_files(files: value, flat:, path: qualified_path)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
flat
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def flattened_files
|
|
193
|
+
@flattened_files || flatten_files(files:)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def io_stream?(file_or_path)
|
|
197
|
+
super || file_or_path.is_a?(MockTempfile)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def matches_globbed_pattern?(entry_names:, pattern_strings:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
201
|
+
prefix_index = pattern_strings.index { |str| str.include?('**') }
|
|
202
|
+
prefix_strings = pattern_strings[...prefix_index]
|
|
203
|
+
prefix_count = prefix_strings.length
|
|
204
|
+
suffix_index = pattern_strings.rindex { |str| str.include?('**') }
|
|
205
|
+
suffix_strings = pattern_strings[(1 + suffix_index)..]
|
|
206
|
+
suffix_count = suffix_strings.length
|
|
207
|
+
|
|
208
|
+
return false unless entry_names.length >= prefix_count + suffix_count
|
|
209
|
+
|
|
210
|
+
entry_names[...prefix_count]
|
|
211
|
+
.zip(prefix_strings)
|
|
212
|
+
.concat(entry_names[-suffix_count...].zip(suffix_strings))
|
|
213
|
+
.all? do |entry_name, pattern_string|
|
|
214
|
+
matches_pattern_string?(entry_name:, pattern_string:)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def matches_pattern?(file_path:, pattern:)
|
|
219
|
+
entry_names = file_path.split(File::SEPARATOR)
|
|
220
|
+
pattern_strings = pattern.split(File::SEPARATOR)
|
|
221
|
+
|
|
222
|
+
if pattern_strings.any? { |str| str.include?('**') }
|
|
223
|
+
return matches_globbed_pattern?(entry_names:, pattern_strings:)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
return false unless entry_names.length == pattern_strings.length
|
|
227
|
+
|
|
228
|
+
entry_names.zip(pattern_strings).all? do |entry_name, pattern_string|
|
|
229
|
+
matches_pattern_string?(entry_name:, pattern_string:)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def matches_pattern_string?(entry_name:, pattern_string:)
|
|
234
|
+
if pattern_string.include?('*')
|
|
235
|
+
pattern_string
|
|
236
|
+
.gsub('.', '\.')
|
|
237
|
+
.gsub(SINGLE_GLOB_PATTERN, '.*')
|
|
238
|
+
.then { |str| Regexp.new(str) }
|
|
239
|
+
.match?(entry_name)
|
|
240
|
+
else
|
|
241
|
+
entry_name == pattern_string
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def resolve_mock(path)
|
|
246
|
+
return unless path.start_with?(root_path)
|
|
247
|
+
|
|
248
|
+
*rest, last = split_path(path)
|
|
249
|
+
|
|
250
|
+
dir = rest.reduce(files) do |dir, segment|
|
|
251
|
+
break if dir[segment].nil? || io_stream?(dir[segment])
|
|
252
|
+
|
|
253
|
+
dir[segment]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
dir&.[](last)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def split_path(path)
|
|
260
|
+
path[(1 + root_path.length)..]
|
|
261
|
+
&.split(File::SEPARATOR) || []
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def write_directory(*dir_names, action:, file_or_path:, recursive: false)
|
|
265
|
+
dir_names.reduce(files) do |dir, dir_name|
|
|
266
|
+
if io_stream?(dir[dir_name])
|
|
267
|
+
raise DirectoryIsAFileError,
|
|
268
|
+
"unable to #{action} #{file_or_path} - directory is a file"
|
|
269
|
+
elsif dir[dir_name].nil? && !recursive
|
|
270
|
+
raise DirectoryNotFoundError,
|
|
271
|
+
"unable to #{action} #{file_or_path} - directory not found"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
dir[dir_name] ||= {}
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def write_mock_file(path, data, file_or_path)
|
|
279
|
+
*dir_names, file_name = split_path(path)
|
|
280
|
+
|
|
281
|
+
directory = write_directory(
|
|
282
|
+
*dir_names,
|
|
283
|
+
action: 'write file',
|
|
284
|
+
file_or_path:,
|
|
285
|
+
recursive: false
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
directory[file_name] = StringIO.new(data.to_s)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def validate_file(file_or_path, as:)
|
|
292
|
+
return if file_or_path.is_a?(MockTempfile)
|
|
293
|
+
|
|
294
|
+
super
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
require 'tempfile'
|
|
6
|
+
|
|
7
|
+
require 'cuprum/cli/dependencies'
|
|
8
|
+
|
|
9
|
+
module Cuprum::Cli::Dependencies
|
|
10
|
+
# Utility wrapping filesystem operations.
|
|
11
|
+
class FileSystem # rubocop:disable Metrics/ClassLength
|
|
12
|
+
autoload :Mock, 'cuprum/cli/dependencies/file_system/mock'
|
|
13
|
+
|
|
14
|
+
# Abstract class for exceptions raised by failing file system operations.
|
|
15
|
+
class FileError < StandardError; end
|
|
16
|
+
|
|
17
|
+
# Exception raised when attempting to access a file as a directory.
|
|
18
|
+
class DirectoryIsAFileError < FileError; end
|
|
19
|
+
|
|
20
|
+
# Exception raised when attempting to access a non-existent directory.
|
|
21
|
+
class DirectoryNotFoundError < FileError; end
|
|
22
|
+
|
|
23
|
+
# Exception raised when attempting to access a directory as a file.
|
|
24
|
+
class FileIsADirectoryError < FileError; end
|
|
25
|
+
|
|
26
|
+
# Exception raised when attempting to access a non-existent file.
|
|
27
|
+
class FileNotFoundError < FileError; end
|
|
28
|
+
|
|
29
|
+
# @param root_path [String] the path to the root directory. Defaults to the
|
|
30
|
+
# value of `Dir.pwd`.
|
|
31
|
+
def initialize(root_path: Dir.pwd)
|
|
32
|
+
@root_path = root_path
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [String] the path to the root directory.
|
|
36
|
+
attr_reader :root_path
|
|
37
|
+
|
|
38
|
+
# Creates a directory at the requested path.
|
|
39
|
+
#
|
|
40
|
+
# @param path [String] the path to the directory.
|
|
41
|
+
# @param recursive [true, false] if true, creates any required intermediate
|
|
42
|
+
# directories, equivalent to the -p flag on `mkdir`. Defaults to false.
|
|
43
|
+
#
|
|
44
|
+
# @return [String] the path to the created directory.
|
|
45
|
+
def create_directory(path, recursive: false)
|
|
46
|
+
tools.assertions.validate_name(path, as: 'path')
|
|
47
|
+
|
|
48
|
+
resolved = resolve_path(path)
|
|
49
|
+
|
|
50
|
+
return path if directory?(resolved)
|
|
51
|
+
|
|
52
|
+
handle_create_errors(path) do
|
|
53
|
+
recursive ? FileUtils.mkdir_p(resolved) : FileUtils.mkdir(resolved)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
path
|
|
57
|
+
end
|
|
58
|
+
alias make_directory create_directory
|
|
59
|
+
|
|
60
|
+
# Checks if the requested directory exists.
|
|
61
|
+
#
|
|
62
|
+
# @param path [String] the path to the requested directory.
|
|
63
|
+
#
|
|
64
|
+
# @return [true, false] true if the directory exists and is a directory,
|
|
65
|
+
# otherwise false.
|
|
66
|
+
def directory?(path)
|
|
67
|
+
tools.assertions.validate_name(path, as: 'path')
|
|
68
|
+
|
|
69
|
+
path = resolve_path(path)
|
|
70
|
+
|
|
71
|
+
File.exist?(path) && File.directory?(path)
|
|
72
|
+
end
|
|
73
|
+
alias directory_exists? directory?
|
|
74
|
+
|
|
75
|
+
# @overload each_file
|
|
76
|
+
# Iterates over file names matching the given pattern.
|
|
77
|
+
#
|
|
78
|
+
# @param pattern [String] the file pattern to match.
|
|
79
|
+
#
|
|
80
|
+
# @return [Enumerator<String>] an enumerator over the matching file names.
|
|
81
|
+
#
|
|
82
|
+
# @overload each_file { |file| }
|
|
83
|
+
# Yields each file name matching the given pattern.
|
|
84
|
+
#
|
|
85
|
+
# @param pattern [String] the file pattern to match.
|
|
86
|
+
#
|
|
87
|
+
# @return [nil]
|
|
88
|
+
#
|
|
89
|
+
# @yieldparam [String] the matching file name.
|
|
90
|
+
def each_file(pattern, &)
|
|
91
|
+
return enum_for(:each_file, pattern) unless block_given?
|
|
92
|
+
|
|
93
|
+
path = resolve_path(pattern)
|
|
94
|
+
|
|
95
|
+
Dir[path].each(&)
|
|
96
|
+
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Checks if the requested file exists.
|
|
101
|
+
#
|
|
102
|
+
# @param path [String] the path to the requested file.
|
|
103
|
+
#
|
|
104
|
+
# @return [true, false] true if the file exists and is a file,
|
|
105
|
+
# otherwise false.
|
|
106
|
+
def file?(path)
|
|
107
|
+
tools.assertions.validate_name(path, as: 'path')
|
|
108
|
+
|
|
109
|
+
path = resolve_path(path)
|
|
110
|
+
|
|
111
|
+
File.exist?(path) && File.file?(path)
|
|
112
|
+
end
|
|
113
|
+
alias file_exists? file?
|
|
114
|
+
|
|
115
|
+
# @overload read_file(file)
|
|
116
|
+
# Reads the contents of the given file or IO stream.
|
|
117
|
+
#
|
|
118
|
+
# @param file [IO] the file to read.
|
|
119
|
+
#
|
|
120
|
+
# @return [String] the file contents.
|
|
121
|
+
#
|
|
122
|
+
# @overload read_file(path)
|
|
123
|
+
# Reads the contents of the file at the given path.
|
|
124
|
+
#
|
|
125
|
+
# @param path [String] the file path to read.
|
|
126
|
+
#
|
|
127
|
+
# @return [String] the file contents.
|
|
128
|
+
#
|
|
129
|
+
# @raise [FileIsADirectoryError] if the requested file is a directory.
|
|
130
|
+
# @raise [FileNotFoundError] if the requested file is not found.
|
|
131
|
+
def read_file(file_or_path)
|
|
132
|
+
validate_file(file_or_path, as: 'file')
|
|
133
|
+
|
|
134
|
+
return file_or_path.read if io_stream?(file_or_path)
|
|
135
|
+
|
|
136
|
+
path = resolve_path(file_or_path)
|
|
137
|
+
|
|
138
|
+
File.read(path)
|
|
139
|
+
rescue Errno::EISDIR
|
|
140
|
+
raise FileIsADirectoryError,
|
|
141
|
+
"unable to read file #{file_or_path} - file is a directory"
|
|
142
|
+
rescue Errno::ENOENT
|
|
143
|
+
raise FileNotFoundError,
|
|
144
|
+
"unable to read file #{file_or_path} - file not found"
|
|
145
|
+
end
|
|
146
|
+
alias read read_file
|
|
147
|
+
|
|
148
|
+
# Creates a tempfile and passes it to the block.
|
|
149
|
+
#
|
|
150
|
+
# @yieldparam [File] the generated tempfile.
|
|
151
|
+
#
|
|
152
|
+
# @return [Object] the value returned by the block.
|
|
153
|
+
def with_tempfile(&) = Tempfile.create(&)
|
|
154
|
+
|
|
155
|
+
# @overload write_file(file, data)
|
|
156
|
+
# Writes the data to the given file or IO stream.
|
|
157
|
+
#
|
|
158
|
+
# @param file [IO] the file to write.
|
|
159
|
+
# @param data [String] the data to write.
|
|
160
|
+
#
|
|
161
|
+
# @return [Integer] the number of bytes written.
|
|
162
|
+
#
|
|
163
|
+
# @overload write_file(path, data)
|
|
164
|
+
# Writes the data to the file at the given path.
|
|
165
|
+
#
|
|
166
|
+
# @param path [String] the file path to write.
|
|
167
|
+
# @param data [String] the data to write.
|
|
168
|
+
#
|
|
169
|
+
# @return [Integer] the number of bytes written.
|
|
170
|
+
#
|
|
171
|
+
# @raise [DirectoryIsAFileError] if the path to the requested file
|
|
172
|
+
# includes an existing file.
|
|
173
|
+
# @raise [DirectoryNotFoundError] if the directory for the requested file
|
|
174
|
+
# is not found.
|
|
175
|
+
# @raise [FileIsADirectoryError] if the requested file is a directory.
|
|
176
|
+
def write_file(file_or_path, data)
|
|
177
|
+
validate_file(file_or_path, as: 'file')
|
|
178
|
+
|
|
179
|
+
return file_or_path.write(data) if io_stream?(file_or_path)
|
|
180
|
+
|
|
181
|
+
path = resolve_path(file_or_path)
|
|
182
|
+
|
|
183
|
+
handle_write_errors(file_or_path) { File.write(path, data) }
|
|
184
|
+
end
|
|
185
|
+
alias write write_file
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def empty_file_message(as:)
|
|
190
|
+
tools.assertions.error_message_for('presence', as:)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def handle_create_errors(path)
|
|
194
|
+
yield
|
|
195
|
+
rescue Errno::EEXIST
|
|
196
|
+
raise Cuprum::Cli::Dependencies::FileSystem::DirectoryIsAFileError,
|
|
197
|
+
"unable to create directory #{path} - directory is a file"
|
|
198
|
+
rescue Errno::ENOENT
|
|
199
|
+
raise Cuprum::Cli::Dependencies::FileSystem::DirectoryNotFoundError,
|
|
200
|
+
"unable to create directory #{path} - directory not found"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def handle_write_errors(file_or_path)
|
|
204
|
+
yield
|
|
205
|
+
rescue Errno::EISDIR
|
|
206
|
+
raise FileIsADirectoryError,
|
|
207
|
+
"unable to write file #{file_or_path} - file is a directory"
|
|
208
|
+
rescue Errno::ENOENT
|
|
209
|
+
raise DirectoryNotFoundError,
|
|
210
|
+
"unable to write file #{file_or_path} - directory not found"
|
|
211
|
+
rescue Errno::ENOTDIR
|
|
212
|
+
raise DirectoryIsAFileError,
|
|
213
|
+
"unable to write file #{file_or_path} - directory is a file"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def invalid_file_message(as:)
|
|
217
|
+
"#{as} is not a String or IO stream"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def io_stream?(file_or_path)
|
|
221
|
+
file_or_path.is_a?(IO) || file_or_path.is_a?(StringIO)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def resolve_path(path)
|
|
225
|
+
return path if File.absolute_path?(path)
|
|
226
|
+
|
|
227
|
+
File.expand_path(File.join(root_path, path))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def tools
|
|
231
|
+
SleepingKingStudios::Tools::Toolbelt.instance
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def validate_file(file_or_path, as:)
|
|
235
|
+
case file_or_path
|
|
236
|
+
in IO | StringIO
|
|
237
|
+
nil
|
|
238
|
+
in nil | ''
|
|
239
|
+
raise ArgumentError, empty_file_message(as:)
|
|
240
|
+
in String # rubocop:disable Lint/DuplicateBranch
|
|
241
|
+
nil
|
|
242
|
+
else
|
|
243
|
+
raise ArgumentError, invalid_file_message(as:)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/cli/dependencies/standard_io'
|
|
4
|
+
|
|
5
|
+
module Cuprum::Cli::Dependencies
|
|
6
|
+
# Helper methods for reading from and writing to standard inputs and outputs.
|
|
7
|
+
module StandardIo::Helpers
|
|
8
|
+
# String input values that will be mapped to a boolean false.
|
|
9
|
+
FALSY_VALUES = Set.new(%w[f false n no]).freeze
|
|
10
|
+
|
|
11
|
+
# Pattern matching a valid integer input.
|
|
12
|
+
INTEGER_PATTERN = /\A-?\d+([\d_,]+\d)?\z/
|
|
13
|
+
|
|
14
|
+
# String input values that will be mapped to a boolean true.
|
|
15
|
+
TRUTHY_VALUES = Set.new(%w[t true y yes]).freeze
|
|
16
|
+
|
|
17
|
+
# @overload ask(prompt = nil, caret: true, format: nil, strip: true, **options)
|
|
18
|
+
# Requests an input from the input stream.
|
|
19
|
+
#
|
|
20
|
+
# @param prompt [String, nil] the prompt to display to the user, if any.
|
|
21
|
+
# @param options [Hash] options for requesting the input.
|
|
22
|
+
#
|
|
23
|
+
# @option options caret [true, false] if true, prints a caret "> " to the
|
|
24
|
+
# output stream after the prompt. Defaults to true when the newline
|
|
25
|
+
# option is true, otherwise false.
|
|
26
|
+
# @option options format [String, Symbol] the expected format of the
|
|
27
|
+
# input. Valid values are :string (the default), :boolean, and :integer.
|
|
28
|
+
# The input string will be transformed into the given format, or an
|
|
29
|
+
# exception raised if the value cannot be transformed.
|
|
30
|
+
# @option options newline [true, false] if true, a newline will be printed
|
|
31
|
+
# after the prompt if a prompt is given.
|
|
32
|
+
# @option options strip [true, false] if true, strips the trailing newline
|
|
33
|
+
# from the input. Defaults to true.
|
|
34
|
+
#
|
|
35
|
+
# @return [String, Integer, true, false, nil] the received and formatted
|
|
36
|
+
# input value, or nil if the input value was empty.
|
|
37
|
+
def ask( # rubocop:disable Metrics/ParameterLists
|
|
38
|
+
prompt = nil,
|
|
39
|
+
caret: nil,
|
|
40
|
+
format: nil,
|
|
41
|
+
newline: true,
|
|
42
|
+
strip: true,
|
|
43
|
+
**
|
|
44
|
+
)
|
|
45
|
+
validate_prompt(prompt)
|
|
46
|
+
display_prompt(caret:, newline:, prompt:)
|
|
47
|
+
|
|
48
|
+
value = standard_io.read_input&.then { |str| strip ? str.strip : str }
|
|
49
|
+
|
|
50
|
+
return if value.nil? || value.empty?
|
|
51
|
+
return value if format.nil?
|
|
52
|
+
|
|
53
|
+
send(:"format_#{format}", value)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @overload say(message, newline: true, quiet: false, verbose: false, **options)
|
|
57
|
+
# Prints a message to the output stream.
|
|
58
|
+
#
|
|
59
|
+
# @param message [String] the message to print.
|
|
60
|
+
# @param options [Hash] options for printing the message.
|
|
61
|
+
#
|
|
62
|
+
# @option options newline [true, false] if true, appends a newline to the
|
|
63
|
+
# message if the message does not end with a newline. Defaults to true.
|
|
64
|
+
# @option options quiet [true, false] if true, prints the message even if
|
|
65
|
+
# the command has the :quiet option enabled. Defaults to false. Ignored
|
|
66
|
+
# if
|
|
67
|
+
# the command does not support the :quiet option.
|
|
68
|
+
# @option options verbose [true, false] if true, prints the message only
|
|
69
|
+
# if the command has the :verbose option enabled. Defaults to false.
|
|
70
|
+
# Ignored if the command does not support the :verbose option.
|
|
71
|
+
#
|
|
72
|
+
# @return [nil]
|
|
73
|
+
def say(message, newline: true, **)
|
|
74
|
+
validate_message(message)
|
|
75
|
+
|
|
76
|
+
standard_io.write_output(message, newline:)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @overload warn(message, **options)
|
|
80
|
+
# Prints a message to the error stream.
|
|
81
|
+
#
|
|
82
|
+
# @param message [String] the message to print.
|
|
83
|
+
# @param options [Hash] options for printing the message.
|
|
84
|
+
#
|
|
85
|
+
# @option options newline [true, false] if true, appends a newline to the
|
|
86
|
+
# message if the message does not end with a newline. Defaults to true.
|
|
87
|
+
#
|
|
88
|
+
# @return [nil]
|
|
89
|
+
def warn(message, newline: true, **)
|
|
90
|
+
validate_message(message)
|
|
91
|
+
|
|
92
|
+
standard_io.write_error(message, newline:)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def display_prompt(caret:, newline:, prompt:)
|
|
98
|
+
standard_io.write_output(prompt, newline:) if prompt
|
|
99
|
+
|
|
100
|
+
return unless caret.nil? ? newline : caret
|
|
101
|
+
|
|
102
|
+
standard_io.write_output('> ', newline: false)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def format_boolean(value)
|
|
106
|
+
Cuprum::Cli::Coercion.coerce_boolean(value.strip)
|
|
107
|
+
rescue Cuprum::Cli::Coercion::CoercionError
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def format_integer(value)
|
|
112
|
+
Cuprum::Cli::Coercion.coerce_integer(value.strip)
|
|
113
|
+
rescue Cuprum::Cli::Coercion::CoercionError
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def tools = SleepingKingStudios::Tools::Toolbelt.instance
|
|
118
|
+
|
|
119
|
+
def validate_message(message)
|
|
120
|
+
tools.assertions.validate_instance_of(
|
|
121
|
+
message,
|
|
122
|
+
as: 'message',
|
|
123
|
+
expected: String
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def validate_prompt(prompt)
|
|
128
|
+
return if prompt.nil?
|
|
129
|
+
|
|
130
|
+
tools.assertions.validate_instance_of(
|
|
131
|
+
prompt,
|
|
132
|
+
as: 'prompt',
|
|
133
|
+
expected: String
|
|
134
|
+
)
|
|
135
|
+
tools.assertions.validate_presence(prompt, as: 'prompt')
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|