rvgp 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +23 -0
- data/LICENSE +504 -0
- data/README.md +223 -0
- data/Rakefile +32 -0
- data/bin/rvgp +8 -0
- data/lib/rvgp/application/config.rb +159 -0
- data/lib/rvgp/application/descendant_registry.rb +122 -0
- data/lib/rvgp/application/status_output.rb +139 -0
- data/lib/rvgp/application.rb +170 -0
- data/lib/rvgp/base/command.rb +457 -0
- data/lib/rvgp/base/grid.rb +531 -0
- data/lib/rvgp/base/reader.rb +29 -0
- data/lib/rvgp/base/reconciler.rb +434 -0
- data/lib/rvgp/base/validation.rb +261 -0
- data/lib/rvgp/commands/cashflow.rb +160 -0
- data/lib/rvgp/commands/grid.rb +70 -0
- data/lib/rvgp/commands/ireconcile.rb +95 -0
- data/lib/rvgp/commands/new_project.rb +296 -0
- data/lib/rvgp/commands/plot.rb +41 -0
- data/lib/rvgp/commands/publish_gsheets.rb +83 -0
- data/lib/rvgp/commands/reconcile.rb +58 -0
- data/lib/rvgp/commands/rotate_year.rb +202 -0
- data/lib/rvgp/commands/validate_journal.rb +59 -0
- data/lib/rvgp/commands/validate_system.rb +44 -0
- data/lib/rvgp/commands.rb +160 -0
- data/lib/rvgp/dashboard.rb +252 -0
- data/lib/rvgp/fakers/fake_feed.rb +245 -0
- data/lib/rvgp/fakers/fake_journal.rb +57 -0
- data/lib/rvgp/fakers/fake_reconciler.rb +88 -0
- data/lib/rvgp/fakers/faker_helpers.rb +25 -0
- data/lib/rvgp/gem.rb +80 -0
- data/lib/rvgp/journal/commodity.rb +453 -0
- data/lib/rvgp/journal/complex_commodity.rb +214 -0
- data/lib/rvgp/journal/currency.rb +101 -0
- data/lib/rvgp/journal/journal.rb +141 -0
- data/lib/rvgp/journal/posting.rb +156 -0
- data/lib/rvgp/journal/pricer.rb +267 -0
- data/lib/rvgp/journal.rb +24 -0
- data/lib/rvgp/plot/gnuplot.rb +478 -0
- data/lib/rvgp/plot/google-drive/output_csv.rb +44 -0
- data/lib/rvgp/plot/google-drive/output_google_sheets.rb +434 -0
- data/lib/rvgp/plot/google-drive/sheet.rb +67 -0
- data/lib/rvgp/plot.rb +293 -0
- data/lib/rvgp/pta/hledger.rb +237 -0
- data/lib/rvgp/pta/ledger.rb +308 -0
- data/lib/rvgp/pta.rb +311 -0
- data/lib/rvgp/reconcilers/csv_reconciler.rb +424 -0
- data/lib/rvgp/reconcilers/journal_reconciler.rb +41 -0
- data/lib/rvgp/reconcilers/shorthand/finance_gem_hacks.rb +48 -0
- data/lib/rvgp/reconcilers/shorthand/international_atm.rb +152 -0
- data/lib/rvgp/reconcilers/shorthand/investment.rb +144 -0
- data/lib/rvgp/reconcilers/shorthand/mortgage.rb +195 -0
- data/lib/rvgp/utilities/grid_query.rb +190 -0
- data/lib/rvgp/utilities/yaml.rb +131 -0
- data/lib/rvgp/utilities.rb +44 -0
- data/lib/rvgp/validations/balance_validation.rb +68 -0
- data/lib/rvgp/validations/duplicate_tags_validation.rb +48 -0
- data/lib/rvgp/validations/uncategorized_validation.rb +15 -0
- data/lib/rvgp.rb +66 -0
- data/resources/README.MD/2022-cashflow-google.png +0 -0
- data/resources/README.MD/2022-cashflow.png +0 -0
- data/resources/README.MD/all-wealth-growth-google.png +0 -0
- data/resources/README.MD/all-wealth-growth.png +0 -0
- data/resources/gnuplot/default.yml +80 -0
- data/resources/i18n/en.yml +192 -0
- data/resources/iso-4217-currencies.json +171 -0
- data/resources/skel/Rakefile +5 -0
- data/resources/skel/app/grids/cashflow_grid.rb +27 -0
- data/resources/skel/app/grids/monthly_income_and_expenses_grid.rb +25 -0
- data/resources/skel/app/grids/wealth_growth_grid.rb +35 -0
- data/resources/skel/app/plots/cashflow.yml +33 -0
- data/resources/skel/app/plots/monthly-income-and-expenses.yml +17 -0
- data/resources/skel/app/plots/wealth-growth.yml +20 -0
- data/resources/skel/config/csv-format-acme-checking.yml +9 -0
- data/resources/skel/config/google-secrets.yml +5 -0
- data/resources/skel/config/rvgp.yml +0 -0
- data/resources/skel/journals/prices.db +0 -0
- data/rvgp.gemspec +6 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty.xml +383 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty2.xml +428 -0
- data/test/test_command_base.rb +61 -0
- data/test/test_commodity.rb +270 -0
- data/test/test_csv_reconciler.rb +60 -0
- data/test/test_currency.rb +24 -0
- data/test/test_fake_feed.rb +228 -0
- data/test/test_fake_journal.rb +98 -0
- data/test/test_fake_reconciler.rb +60 -0
- data/test/test_journal_parse.rb +545 -0
- data/test/test_ledger.rb +102 -0
- data/test/test_plot.rb +133 -0
- data/test/test_posting.rb +50 -0
- data/test/test_pricer.rb +139 -0
- data/test/test_pta_adapter.rb +575 -0
- data/test/test_utilities.rb +45 -0
- metadata +268 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../rvgp'
|
|
4
|
+
require_relative '../application/descendant_registry'
|
|
5
|
+
|
|
6
|
+
module RVGP
|
|
7
|
+
module Base
|
|
8
|
+
# If you're looking to write your own rvgp commands, or if you wish to add a rake task - this is the start of that
|
|
9
|
+
# endeavor.
|
|
10
|
+
#
|
|
11
|
+
# All of the built-in rvgp commands are descendants of this Base class. And, the easiest way to get started in
|
|
12
|
+
# writing your own, is simply to emulate one of these examples. You can see links to these examples listed under
|
|
13
|
+
# {RVGP::Commands}.
|
|
14
|
+
#
|
|
15
|
+
# When you're ready to start typing out your code, just place this code in a .rb file under the app/commands
|
|
16
|
+
# directory of your project - and rvgp will pick it up from there. An instance of a Command, that inherits from
|
|
17
|
+
# this base, is initialized with the parsed contents of the command line, for any case when a user invokes the
|
|
18
|
+
# command by its name on the CLI.
|
|
19
|
+
#
|
|
20
|
+
# The content below documents the argument handling, rake workflow, and related functionality available to you
|
|
21
|
+
# in your commands.
|
|
22
|
+
# @attr_reader [Array<String>] errors This array contains any errors that were encountered, when attempting to
|
|
23
|
+
# initialize this command.
|
|
24
|
+
# @attr_reader [Hash<Symbol,TrueClass, String>] options A hash of pairs, with keys being set to the 'long' form
|
|
25
|
+
# of any options that were passed on the command line. And
|
|
26
|
+
# with values consisting of either 'string' (for the case
|
|
27
|
+
# of a ''--option=value') or 'True' for the prescense of
|
|
28
|
+
# an option in the short or long form ("-l" or "--long")
|
|
29
|
+
# @attr_reader [<Object>] targets The parsed targets, that were encountered, for this command. Note that this Array
|
|
30
|
+
# may contain just about any object whatsoever, depending on how the Target for
|
|
31
|
+
# a command is written.
|
|
32
|
+
class Command
|
|
33
|
+
# Targets are, as the name would imply, a command line argument, that isn't prefixed with one or more dashes.
|
|
34
|
+
# Whereas some arguments are program options, targets are typically a specific subject or destination, which
|
|
35
|
+
# the command is applied to.
|
|
36
|
+
#
|
|
37
|
+
# This base class offers common functions for navigating targets, and identifying targets on the command line.
|
|
38
|
+
# This is a base class, which would generally find an inheritor inside a specific command's implementation.
|
|
39
|
+
# @attr_reader [String] name The target name, as it would be expected to be found on the CLI
|
|
40
|
+
# @attr_reader [String] status_name The target name, as it would be expected to appear in the status output,
|
|
41
|
+
# which is generally displayed during the processing of this target during
|
|
42
|
+
# the rake process and/or during an rvgp-triggered process.
|
|
43
|
+
# @attr_reader [String] description A description of this target. Mostly this is used by rake, to describe
|
|
44
|
+
# this target in the 'rake -T' output.
|
|
45
|
+
class Target
|
|
46
|
+
attr_reader :name, :status_name, :description
|
|
47
|
+
|
|
48
|
+
# Create a new Target
|
|
49
|
+
# @param [String] name see {RVGP::Base::Command::Target#name}
|
|
50
|
+
# @param [String] status_name see {RVGP::Base::Command::Target#status_name}
|
|
51
|
+
def initialize(name, status_name = nil)
|
|
52
|
+
@name = name
|
|
53
|
+
@status_name = status_name
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns true, if the provided identifier matches this target
|
|
57
|
+
# @param [String] identifier A target that was encountered on the CLI
|
|
58
|
+
# @return [TrueClass, FalseClass] whether we're the target specified
|
|
59
|
+
def matches?(identifier)
|
|
60
|
+
File.fnmatch? identifier, name
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Find the target that matches the provided string
|
|
64
|
+
# @param [String] str A string which expresses which needle, we want to find, in this haystack.
|
|
65
|
+
# @return [Target] The target we matched this string against.
|
|
66
|
+
def self.from_s(str)
|
|
67
|
+
all.find_all { |target| target.matches? str }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# This is an implementation of Target, that matches Reconcilers.
|
|
72
|
+
#
|
|
73
|
+
# This class allows any of the current project's reconcilers to match a target. And, such targets can be selected
|
|
74
|
+
# by way of a:
|
|
75
|
+
# - full reconciler path
|
|
76
|
+
# - reconciler file basename (without the full path)
|
|
77
|
+
# - the reconciler's from field
|
|
78
|
+
# - the reconciler's label field
|
|
79
|
+
# - the reconciler's input file
|
|
80
|
+
# - the reconciler's output file
|
|
81
|
+
#
|
|
82
|
+
# Any class that operates by way of a reconciler-defined target, can use this implementation, in lieu of
|
|
83
|
+
# re-implementing the wheel.
|
|
84
|
+
class ReconcilerTarget < RVGP::Base::Command::Target
|
|
85
|
+
# Create a new ReconcilerTarget
|
|
86
|
+
# @param [RVGP::Base::Reconciler] reconciler An instance of either {RVGP::Reconcilers::CsvReconciler}, or
|
|
87
|
+
# {RVGP::Reconcilers::JournalReconciler}, to use as the basis
|
|
88
|
+
# for this target.
|
|
89
|
+
def initialize(reconciler)
|
|
90
|
+
super reconciler.as_taskname, reconciler.label
|
|
91
|
+
@reconciler = reconciler
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# (see RVGP::Base::Command::Target#matches?)
|
|
95
|
+
def matches?(identifier)
|
|
96
|
+
@reconciler.matches_argument? identifier
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# (see RVGP::Base::Command::Target#description)
|
|
100
|
+
def description
|
|
101
|
+
I18n.t format('commands.%s.target_description', self.class.command), input_file: @reconciler.input_file
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# All possible Reconciler Targets that the project has defined.
|
|
105
|
+
# @return [Array<RVGP::Base::Command::ReconcilerTarget>] A collection of targets.
|
|
106
|
+
def self.all
|
|
107
|
+
RVGP.app.reconcilers.map { |reconciler| new reconciler }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# This is a little goofy. But, it exists as a hack to support dispatching this target via the
|
|
111
|
+
# {RVGP::Base::Command::ReconcilerTarget.command} method. You can see an example of this at work in the
|
|
112
|
+
# {https://github.com/brighton36/rvgp/blob/main/lib/rvgp/commands/reconcile.rb reconcile.rb} file.
|
|
113
|
+
# @param [Symbol] underscorized_command_name The command to return, when
|
|
114
|
+
# {RVGP::Base::Command::ReconcilerTarget.command} is called.
|
|
115
|
+
def self.for_command(underscorized_command_name)
|
|
116
|
+
@for_command = underscorized_command_name
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Returns which command this class is defined for. See the note in
|
|
120
|
+
# #{RVGP::Base::Command::ReconcilerTarget.for_command}.
|
|
121
|
+
# @return [Symbol] The command this target is relevant for.
|
|
122
|
+
def self.command
|
|
123
|
+
@for_command
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# This is an implementation of Target, that matches Plots.
|
|
128
|
+
#
|
|
129
|
+
# This class allows any of the current project's plots, to match a target, based on their name and variants.
|
|
130
|
+
#
|
|
131
|
+
# Any class that operates by way of a plot-defined target, can use this implementation, in lieu of
|
|
132
|
+
# re-implementing the wheel.
|
|
133
|
+
# @attr_reader [RVGP::Plot] plot An instance of the plot that offers our :name variant
|
|
134
|
+
class PlotTarget < RVGP::Base::Command::Target
|
|
135
|
+
attr_reader :plot
|
|
136
|
+
|
|
137
|
+
# Create a new PlotTarget
|
|
138
|
+
# @param [String] name A plot variant
|
|
139
|
+
# @param [RVGP::Plot] plot A plot instance which will handle this variant
|
|
140
|
+
def initialize(name, plot)
|
|
141
|
+
super name, name
|
|
142
|
+
@plot = plot
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @!visibility private
|
|
146
|
+
def description
|
|
147
|
+
I18n.t 'commands.plot.target_description', name: name
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# @!visibility private
|
|
151
|
+
def uptodate?
|
|
152
|
+
# I'm not crazy about listing the extension here. Possibly that should come
|
|
153
|
+
# from the plot object. It's conceivable in the future, that we'll use
|
|
154
|
+
# more than one extension here...
|
|
155
|
+
FileUtils.uptodate? @plot.output_file(@name, 'gpi'), [@plot.path] + @plot.variant_files(@name)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# All possible Plot Targets that the project has defined.
|
|
159
|
+
# @return [Array<RVGP::Base::Command::PlotTarget>] A collection of targets.
|
|
160
|
+
def self.all
|
|
161
|
+
RVGP::Plot.all(RVGP.app.config.project_path('app/plots')).map do |plot|
|
|
162
|
+
plot.variants.map { |params| new params[:name], plot }
|
|
163
|
+
end.flatten
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Option(s) are, as the name would imply, a command line option, that is prefixed with one or more dashes.
|
|
168
|
+
# Whereas some arguments are program targets, options typically expresses a global program setting, to take
|
|
169
|
+
# effect during this execution.
|
|
170
|
+
#
|
|
171
|
+
# Some options are binaries, and are presumed 'off' if unspecified. Other options are key/value pairs, separated
|
|
172
|
+
# by an equal sign or space, in a form such as "-d ~/ledger" or "--dir=~/ledger". Option keys are expected to
|
|
173
|
+
# exist in both a short and long form. In the previous example, both the "-d" and "--dir" examples are identical.
|
|
174
|
+
# The "-d" form is a short form and "--dir" is a long form, of the same Option.
|
|
175
|
+
#
|
|
176
|
+
# This class offers common functions for specifying and parsing options on the command line, as well as
|
|
177
|
+
# for producing the documentation on an option.
|
|
178
|
+
# @attr_reader [Symbol] short A one character code, which identifies this option
|
|
179
|
+
# @attr_reader [Symbol] long A multi-character code, which identifies this option
|
|
180
|
+
class Option
|
|
181
|
+
# This error is raised when an option is encountered on the CLI, and the string terminated, before a value
|
|
182
|
+
# could be parsed.
|
|
183
|
+
class UnexpectedEndOfArgs < StandardError; end
|
|
184
|
+
|
|
185
|
+
attr_reader :short, :long
|
|
186
|
+
|
|
187
|
+
# Create a new Option
|
|
188
|
+
# @param [String] short see {RVGP::Base::Command::Option#short}
|
|
189
|
+
# @param [String] long see {RVGP::Base::Command::Option#long}
|
|
190
|
+
# @param [Hash] options additional parameters to configure this Option with
|
|
191
|
+
# @option options [TrueClass,FalseClass] :has_value (false) This flag indicates that this option is expected to
|
|
192
|
+
# have a corresponding value, for its key.
|
|
193
|
+
def initialize(long, short, options = {})
|
|
194
|
+
@short = short.to_sym
|
|
195
|
+
@long = long.to_sym
|
|
196
|
+
@has_value = options[:has_value] if options.key? :has_value
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Returns true, if either our short or long form, equals the provided string
|
|
200
|
+
# @param [String] str an option. This is expected to include one or more dashes.
|
|
201
|
+
# @return [TrueClass,FalseClass] Whether or not we can handle the provided option.
|
|
202
|
+
def matches?(str)
|
|
203
|
+
["--#{long}", "-#{short}"].include? str
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns true, if we expect our key to be paired with a value. This property is specified in the :has_value
|
|
207
|
+
# option in the constructor.
|
|
208
|
+
# @return [TrueClass,FalseClass] Whether or not we expect a pair
|
|
209
|
+
def value?
|
|
210
|
+
!@has_value.nil?
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Given program arguments, and an array of options that we wish to support, return the options and arguments
|
|
214
|
+
# that were encountered.
|
|
215
|
+
# @param [Array<RVGP::Base::Command::Option>] options The options to that we want to parse, from out of the
|
|
216
|
+
# provided args
|
|
217
|
+
# @param [Array<String>] args Program arguments, as would be provided by a typical ARGV
|
|
218
|
+
# @return [Array<Hash<Symbol,Object>,Array<String>>] A two-element array. The first element is a Hash of Symbols
|
|
219
|
+
# To Objects (Either TrueClass or String). The second is an
|
|
220
|
+
# Array of Strings. The first element represents what options
|
|
221
|
+
# were parsed, with the key for those options being
|
|
222
|
+
# represented by their :long form (regardless of what was
|
|
223
|
+
# encountered) The second element contains the targets that
|
|
224
|
+
# were encountered.
|
|
225
|
+
def self.remove_options_from_args(options, args)
|
|
226
|
+
ret_args = []
|
|
227
|
+
ret_options = {}
|
|
228
|
+
|
|
229
|
+
i = 0
|
|
230
|
+
until i >= args.length
|
|
231
|
+
arg = args[i]
|
|
232
|
+
arg_value = nil
|
|
233
|
+
|
|
234
|
+
if /\A([^=]+)=([^ ]+)/.match arg
|
|
235
|
+
arg = ::Regexp.last_match 1
|
|
236
|
+
arg_value = ::Regexp.last_match 2
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
option = options.find { |opt| opt.matches? arg }
|
|
240
|
+
|
|
241
|
+
if option
|
|
242
|
+
ret_options[option.long] = if option.value?
|
|
243
|
+
if arg_value.nil?
|
|
244
|
+
if i + 1 >= args.length
|
|
245
|
+
raise UnexpectedEndOfArgs, I18n.t('error.end_of_args')
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
i += 1
|
|
249
|
+
args[i]
|
|
250
|
+
else
|
|
251
|
+
arg_value
|
|
252
|
+
end
|
|
253
|
+
else
|
|
254
|
+
true
|
|
255
|
+
end
|
|
256
|
+
else
|
|
257
|
+
ret_args << args[i]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
i += 1
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
[ret_options, ret_args]
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
include RVGP::Application::DescendantRegistry
|
|
268
|
+
|
|
269
|
+
register_descendants RVGP, :commands
|
|
270
|
+
|
|
271
|
+
attr_reader :errors, :options, :targets
|
|
272
|
+
|
|
273
|
+
# This is shortcut to a --all/-a option, which is common across the built-in rvgp commands
|
|
274
|
+
OPTION_ALL = %i[all a].freeze
|
|
275
|
+
# This is shortcut to a --list/-l option, which is common across the built-in rvgp commands
|
|
276
|
+
OPTION_LIST = %i[list l].freeze
|
|
277
|
+
|
|
278
|
+
# Create a new Command, suitable for execution, and initialized with command line arguments.
|
|
279
|
+
# @param [Array<String>] args The arguments that will govern this command's execution, as they would be expected
|
|
280
|
+
# to be found in ARGV.
|
|
281
|
+
def initialize(*args)
|
|
282
|
+
@errors = []
|
|
283
|
+
@options = {}
|
|
284
|
+
@targets = []
|
|
285
|
+
|
|
286
|
+
# We'll cast the arguments to one of these, instead of storing strings
|
|
287
|
+
target_klass = self.class.const_get('Target')
|
|
288
|
+
|
|
289
|
+
@options, remainders = Option.remove_options_from_args self.class.options, args
|
|
290
|
+
|
|
291
|
+
missing_targets = []
|
|
292
|
+
remainders.each do |remainder|
|
|
293
|
+
if target_klass
|
|
294
|
+
targets = target_klass.from_s remainder
|
|
295
|
+
|
|
296
|
+
if targets
|
|
297
|
+
@targets += targets
|
|
298
|
+
else
|
|
299
|
+
missing_targets << remainder
|
|
300
|
+
end
|
|
301
|
+
else
|
|
302
|
+
@targets << remainder
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
if options[:list] && target_klass
|
|
307
|
+
indent = I18n.t 'status.indicators.indent'
|
|
308
|
+
puts ([RVGP.pastel.bold(I18n.t(format('commands.%s.list_targets', self.class.name)))] +
|
|
309
|
+
target_klass.all.map { |target| indent + target.name }).join("\n")
|
|
310
|
+
exit
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
@targets = target_klass.all if options[:all] && target_klass
|
|
314
|
+
|
|
315
|
+
@errors << I18n.t('error.no_targets') if @targets.empty?
|
|
316
|
+
@errors << I18n.t('error.missing_target', targets: missing_targets.join(', ')) unless missing_targets.empty?
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Indicates whether we can execute this command, given the provided arguments
|
|
320
|
+
# @return [TrueClass,FalseClass] Returns true if there were no problems during initialization
|
|
321
|
+
def valid?
|
|
322
|
+
errors.empty?
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Executes the command, using the provided options, for each of the targets provided.
|
|
326
|
+
# @return [void]
|
|
327
|
+
def execute!
|
|
328
|
+
execute_each_target
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
private
|
|
332
|
+
|
|
333
|
+
def execute_each_target
|
|
334
|
+
# This keeps things DRY for the case of commands such as reconcile, which
|
|
335
|
+
# use the stdout option
|
|
336
|
+
targets.each { |target| target.execute options }
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
class << self
|
|
340
|
+
# This method exists as a shortcut for inheriting classes, to use, in defining what options their command
|
|
341
|
+
# supports. This method expects a variable amount of arrays. With, each of those arrays
|
|
342
|
+
# expected to contain a :short and :long symbol, and optionally a third Hash element, specifying initialize
|
|
343
|
+
# options.
|
|
344
|
+
#
|
|
345
|
+
# Each of these arguments are supplied to {RVGP::Base::Command::Option#initialize}.
|
|
346
|
+
# {RVGP::Base::Command::OPTION_ALL} and {RVGP::Base::Command::OPTION_LIST} are common parameters to supply as
|
|
347
|
+
# arguments to this method.
|
|
348
|
+
# @param [Array<Array<Symbol,Hash>>] args An array, of pairs of [:long, :short] Symbol(s).
|
|
349
|
+
def accepts_options(*args)
|
|
350
|
+
@options = args.map { |option_args| Option.new(*option_args) }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Return the options that have been defined for this command
|
|
354
|
+
# @return [Array<RVGP::Base::Command::Option] the options this command handles
|
|
355
|
+
def options
|
|
356
|
+
@options || []
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# This module contains helpers methods, for commands, that want to be inserted into the rake process. By including
|
|
361
|
+
# this module in your command, you'll gain access to {RVGP::Base::Command::RakeTask::ClassMethods#rake_tasks},
|
|
362
|
+
# which will append the Target(s) of your command, to the rake process.
|
|
363
|
+
#
|
|
364
|
+
# If custom rake declarations are necessary for your command the
|
|
365
|
+
# {RVGP::Base::Command::RakeTask::ClassMethods#initialize_rake} method can be overridden, in order to make those
|
|
366
|
+
# declarations.
|
|
367
|
+
#
|
|
368
|
+
# Probably you should just head over to {RVGP::Base::Command::RakeTask::ClassMethods} to learn more about this
|
|
369
|
+
# module.
|
|
370
|
+
module RakeTask
|
|
371
|
+
# @!visibility private
|
|
372
|
+
def execute!
|
|
373
|
+
targets.map do |target|
|
|
374
|
+
RVGP.app.logger.info self.class.name, target.status_name do
|
|
375
|
+
warnings, errors = target.execute options
|
|
376
|
+
warnings ||= []
|
|
377
|
+
errors ||= []
|
|
378
|
+
{ warnings: warnings, errors: errors }
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# @!visibility private
|
|
384
|
+
def self.included(klass)
|
|
385
|
+
klass.extend ClassMethods
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# These methods are automatically included by the RakeTask module, and provide
|
|
389
|
+
# helper methods, to the class itself, of the command that RakeTask was included
|
|
390
|
+
# in.
|
|
391
|
+
module ClassMethods
|
|
392
|
+
# The namespace in which this command's targets are defined. This value is
|
|
393
|
+
# set by {RVGP::Base::Command::RakeTask::ClassMethods#rake_tasks}.
|
|
394
|
+
attr_reader :rake_namespace
|
|
395
|
+
|
|
396
|
+
# This method is provided for classes that include this module. Calling this method, with a namespace,
|
|
397
|
+
# ensures that all the targets in the command, are setup as rake tasks inside the provided namespace.
|
|
398
|
+
# @param [Symbol] namespace A prefix, under which this command's targets will be declared in rake.
|
|
399
|
+
# @return [void]
|
|
400
|
+
def rake_tasks(namespace)
|
|
401
|
+
@rake_namespace = namespace
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# @!visibility private
|
|
405
|
+
def task_exec(target)
|
|
406
|
+
error_count = 0
|
|
407
|
+
command = new target.name
|
|
408
|
+
|
|
409
|
+
unless target.uptodate?
|
|
410
|
+
rets = command.execute!
|
|
411
|
+
raise StandardError, 'This should never happen' if rets.length > 1
|
|
412
|
+
|
|
413
|
+
if rets.empty?
|
|
414
|
+
raise StandardError, format('The %<command>s command aborted when trying to run the %<task>s task',
|
|
415
|
+
command: command.class.name,
|
|
416
|
+
task: task.name)
|
|
417
|
+
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
error_count += rets[0][:errors].length
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# NOTE: It would be kind of nice, IMO, if the namespace continued
|
|
424
|
+
# to run, and then failed. Instead of having all tasks in the
|
|
425
|
+
# namespace halt, on an error. I don't know how to do this, without
|
|
426
|
+
# a lot of monkey patching and such.
|
|
427
|
+
# Or, maybe, we could just not use multitask() and instead write
|
|
428
|
+
# our own multitasking loop, which, is a similar pita
|
|
429
|
+
abort if error_count.positive?
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# This method initializes rake tasks in the provided context. This method exists as a default implementation
|
|
433
|
+
# for commands, with which to initialize their rake tasks. Feel free to overload this default behavior in your
|
|
434
|
+
# commands.
|
|
435
|
+
# @param [main] rake_main Typically this is the environment of a Rakefile that was passed onto us via self.
|
|
436
|
+
# @return [void]
|
|
437
|
+
def initialize_rake(rake_main)
|
|
438
|
+
command_klass = self
|
|
439
|
+
|
|
440
|
+
if rake_namespace
|
|
441
|
+
rake_main.instance_eval do
|
|
442
|
+
namespace command_klass.rake_namespace do
|
|
443
|
+
command_klass.const_get('Target').all.each do |target|
|
|
444
|
+
unless Rake::Task.task_defined?(target.name)
|
|
445
|
+
desc target.description
|
|
446
|
+
task(target.name) { |_task, _task_args| command_klass.task_exec(target) }
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|