chieftain 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 17774b03847bead6d36c230eddf5d793cacc9889b35c19e344a8be1a973b0878
4
+ data.tar.gz: 88f35af779073c7ba55f6907075a6ab3f58df2a2c9de05daa14e88e7555c06c6
5
+ SHA512:
6
+ metadata.gz: ea95cf8bba3f6a7cfcbe1b9cb5e310b6e990421fe2ff8669d6c41338c14653a264a0de427f02b8b93c0101ddddab4b61e81b1459674265197291ba940c5941d5
7
+ data.tar.gz: cd99d161b3d5b651717855223c57db1d254ea0c2ecde3ab9656735a9a27ef83b978e376f761aa83887118805eb66f044d2d6d1a82a21a334cab4de1a75429440
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in chieftain.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+ gem "rspec", "~> 3.11"
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ chieftain (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.5.0)
10
+ rake (13.0.6)
11
+ rspec (3.11.0)
12
+ rspec-core (~> 3.11.0)
13
+ rspec-expectations (~> 3.11.0)
14
+ rspec-mocks (~> 3.11.0)
15
+ rspec-core (3.11.0)
16
+ rspec-support (~> 3.11.0)
17
+ rspec-expectations (3.11.0)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.11.0)
20
+ rspec-mocks (3.11.1)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.11.0)
23
+ rspec-support (3.11.0)
24
+
25
+ PLATFORMS
26
+ x86_64-linux
27
+
28
+ DEPENDENCIES
29
+ chieftain!
30
+ rake (~> 13.0)
31
+ rspec (~> 3.11)
32
+
33
+ BUNDLED WITH
34
+ 2.3.7
data/LICENSE.txt ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2022 Peter Wood
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # Chieftain
2
+
3
+ Chieftain is a library that provides an implementation of the Command design
4
+ pattern that attempts to make use of the capabilities of the Ruby language to
5
+ simplify usage. The library is heavily inspired by the
6
+ [Mutations](https://github.com/cypriss/mutations) but also seeks to address
7
+ a few pet peeves with that library.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'chieftain'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle install
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install chieftain
24
+
25
+ ## Usage
26
+
27
+ The Command pattern encapsulates the functionality for a particular process
28
+ allowing it to be de-couple from where that functionality is invoked and to
29
+ allow the functionality to be test independently. With the Chieftain library
30
+ the pattern is implemented by creating a class that derives from the
31
+ ``Chieftain::Command`` class. The example below shows and minimalistic
32
+ command class...
33
+
34
+ ```ruby
35
+ class ExampleCommand < Chieftain::Command
36
+ def perform
37
+ # Your command functionality goes here.
38
+ end
39
+ end
40
+ ```
41
+
42
+ Here the ``ExampleCommand`` class derives from the ``Chieftain::Command`` class
43
+ and provides an implementation of the ``#perform()`` method. The ``#perform()``
44
+ method is where you place the code that performs the work on the command. An
45
+ example of using this class would look as follows...
46
+
47
+ ```ruby
48
+ command = ExampleCommand.new
49
+ result = command.execute
50
+ ```
51
+
52
+ In this case the command takes no parameters bit but see the next section to see
53
+ how parameters are handled by the command. This example also shows how to invoke
54
+ the command functionality by calling the ``#execute()`` method. This method will
55
+ return a ``Chieftain::Command::Result`` instance that provides information on
56
+ the success or failure of the command execution.
57
+
58
+ Commands can fail for a number of reasons, including missing required
59
+ parameters, parameter values failing validation or conversion or because the
60
+ actual command perform code indicates a failure. You can check whether a
61
+ ``Result`` instance represent a successful execution by invoking the
62
+ ``#success?()`` method (or it's inverse ``#failed?()``).
63
+
64
+ If a command has failed then that means it will have one or more errors
65
+ generated during execution. You can access these directly by calling the
66
+ ``#errors()`` method on the ``Result`` object. This returns an ``Array``
67
+ of ``Chieftain::Command::Error`` instances representing the errors for
68
+ the command execution. If you just want error message strings then call the
69
+ ``#error_messages()`` method instead.
70
+
71
+ ### Parameters
72
+
73
+ You can pass parameters to your command by passing a ``Hash`` containing the
74
+ parameters to the command constructor. The keys for this ``Hash`` should be
75
+ ``Symbol``s, with the ``Symbol`` becoming the parameter name, so these will
76
+ also have to adhere to Ruby's method naming requirements.
77
+
78
+ Before you pass parameters to your command you should make the command class
79
+ aware that the parameter is expected. When 'declaring' your parameter to your
80
+ command class you should decide whether the parameter is mandatory or
81
+ optional. Required parameters, as might be expected, need to have a value
82
+ specified for them when the command is created. Optional parameters can
83
+ appear in a parameter list but isn't required to. So, an example of how
84
+ this may look is given below...
85
+
86
+ ```ruby
87
+ class CreatePerson < Chieftain::Command
88
+ required :first_name
89
+ required :last_name
90
+ optional :middle_name
91
+ end
92
+ ```
93
+
94
+ Here the command has two parameters that must be provided when the command is
95
+ instantiated and one that may be provided. So the following are valid ways to
96
+ construct this command...
97
+
98
+ ```ruby
99
+ CreatePerson.new(first_name: "John", last_name: "Smith").execute
100
+ CreatePerson.new(first_name: "Joseph",
101
+ middle_name: "Frank",
102
+ last_name: "Bloggs").execute
103
+ ```
104
+
105
+ The required aspect of a parameter is not checked at construction but is instead
106
+ checked when you try to execute the command. If a required parameter is not
107
+ present in the commands parameter set then an error noting this will be
108
+ registered on the command, validation will fail, the ``#perform()`` method will
109
+ not be invoked and a fail result will be returned.
110
+
111
+ ### Convertors
112
+
113
+ When defining parameters for a command you can also provide an indication of the
114
+ expected type for the parameter. An example of this is shown below...
115
+
116
+ ```ruby
117
+ class CreatePerson < Chieftain::Command
118
+ required :name, type: :string
119
+ optional :age, type: :integer
120
+ end
121
+ ```
122
+
123
+ In this case the command has two parameter defined. The first is expected to
124
+ be a string value and the second to be an integer. If the value actually
125
+ provided for the parameter is not of this type then an attempt will be made
126
+ to coerce to this type. If this effort fails then the command will fail
127
+ validation and return an unsuccessful result.
128
+
129
+ The Chieftain library defines the following types (and associated conversion
130
+ functionality) - :boolean, :float, :integer and :string. It is possible to
131
+ extend this set by defining a custom convertor class and making it available
132
+ to your command class.
133
+
134
+ Convertor classes are any class that provides an implementation for two methods
135
+ called ``#convertible?()`` and ``#convert()``. The ``#convertible()`` method
136
+ takes a single parameter which will be the raw value provided to the command for
137
+ the parameter. The method should determine whether this value can be converted
138
+ to the appropriate type, returning true if that is the case and false otherwise.
139
+ The ``#convert()`` method takes the same parameter but should return a value of
140
+ the appropriate type post conversion.
141
+
142
+ You can make a convertor class available as a type on a command class by
143
+ declaring it using the the ``#add_convertor()`` class method. The following
144
+ is an example of doing this...
145
+
146
+ ```ruby
147
+ # Convertor that converts a time string to the number of seconds since the
148
+ # start of the day.
149
+ class TimeConverter
150
+ def convertible?(value)
151
+ parts = value.to_s.split(":").map(&:to_i)
152
+ parts.length == 3 &&
153
+ (parts[0] >= 0 && parts[0] < 24) &&
154
+ (parts[1] >= 0 && parts[1] < 60) &&
155
+ (parts[2] >= 0 && parts[2] < 60)
156
+ end
157
+
158
+ def convert(value)
159
+ parts = value.to_s.split(":").map(&:to_i)
160
+ (parts[0] * 3600) + (parts[1] * 60) + parts[2]
161
+ end
162
+ end
163
+
164
+ class ExampleCommand < Chieftain::Command
165
+ required :timestamp, type: :time
166
+
167
+ add_convertor :time, TimeConvertor
168
+ end
169
+ ```
170
+
171
+ Here a ``TimeConvertor`` class is first defined. The command then declares a
172
+ ``:timestamp`` parameter and indicates it's ``type`` as ``:time``. After this
173
+ the command 'adds' a convertor by calling the ``#add_convertor()`` class. This
174
+ call takes two parameters. The first is the name to be associated with the new
175
+ convertor. The second is the convertor class.
176
+
177
+ One final note with regards to convertors. Custom convertors declared in a
178
+ parent class will be available in derived classes. Note that, if your derived
179
+ class adds a new convertor with a name that clashes with a convertor declared
180
+ in a parent, the new convertor takes precedence and the one from the parent
181
+ is not available.
182
+
183
+ ### Validations
184
+
185
+ Validations are a mechanism for outlining a set of checks for a command
186
+ parameter. The library defines a set of predefined validations that are
187
+ available for use on every command. Additional validations can be defined
188
+ within a command and specified as applicable to a one or more of the command
189
+ parameters. An example of defining a validation is shown below...
190
+
191
+ ```ruby
192
+ class ExampleCommand < Chieftain::Command
193
+ required :code, type: :string, validations: [:length_check]
194
+
195
+ add_validator(:length_check) do |name, value|
196
+ if value.length != 10
197
+ error("The '#{name}' parameter must be exactly 10 characters in length.")
198
+ end
199
+ end
200
+ end
201
+ ```
202
+
203
+ In this example you can see that a single parameter with the name code is
204
+ defined for the ``ExampleCommand`` class. As part of the definition for this
205
+ parameter we see that the ``validations`` setting has been set to an ``Array``
206
+ containing the single ``Symbol`` ``:length_check``. This is the name of a
207
+ validations that is expected to exist and will be applied to the parameter
208
+ whenever validations take place.
209
+
210
+ Later in the class we can see the definition of the ``:length_check`` validation
211
+ using the ``#add_validator()`` method. This method takes a single parameter
212
+ which is the name of the validation. This must be a ``Symbol`` and validation
213
+ names must be unique within the context of a class.
214
+
215
+ The ``#add_validator()`` method also accepts a block, with the block defining
216
+ the functionality of the validation. This block will get executed within the
217
+ context of the invoking command class instance (i.e. ``self`` will refer to the
218
+ command instance). The block should also accept two parameters. The first is the
219
+ name of the parameter being validated. The second will be the value supplied for
220
+ the parameter.
221
+
222
+ In the example given above the validation checks that the parmaeter value
223
+ provided, which will be a string, must have a length of 10. In the case that the
224
+ value provided does not have this length then an error is register on the
225
+ command instance the validation was invoked by. There is another more concise
226
+ form that can be used to achieve the same result and this is shown in the
227
+ example below...
228
+
229
+ ```ruby
230
+ class ExampleCommand < Chieftain::Command
231
+ required :code, type: :string
232
+
233
+ validate(:code) do |name, value|
234
+ if value.length != 10
235
+ error("The '#{name}' parameter must be exactly 10 characters in length.")
236
+ end
237
+ end
238
+ end
239
+ ```
240
+
241
+ Here we define a validation using the ``#validate()`` method (which is really
242
+ just a synonym for the the ``#add_validator()`` method but is more fitting for
243
+ this form of the code). The validations has the same name as the parameter and
244
+ doing this will cause the command to automatically apply it to the parameter
245
+ when it gets validated.
246
+
247
+ One final note with regards to validations. Custom validations declared in a
248
+ parent class will be available in derived classes. Note that, if your derived
249
+ class adds a new validation with a name that clashes with a validation declared
250
+ in a parent, the new validation takes precedence and the one from the parent
251
+ is not available.
252
+
253
+ ## Development
254
+
255
+ After checking out the repo, run `bin/setup` to install dependencies. You can
256
+ also run `bin/console` for an interactive prompt that will allow you to
257
+ experiment.
258
+
259
+ To install this gem onto your local machine, run `bundle exec rake install`. To
260
+ release a new version, update the version number in `version.rb`, and then run
261
+ `bundle exec rake release`, which will create a git tag for the version, push
262
+ git commits and the created tag, and push the `.gem` file to
263
+ [rubygems.org](https://rubygems.org).
264
+
265
+ ## Contributing
266
+
267
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/chieftain.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1,394 @@
1
+ require "ostruct"
2
+
3
+ module Chieftain
4
+ # An implementation of the Command design pattern that aims to take some
5
+ # advantage of Ruby's enhanced capabilities.
6
+ class Command
7
+ # The type associated with errors that prevent a Command from executing.
8
+ class Error
9
+ def initialize(message, code=nil)
10
+ @code = code
11
+ @message = message
12
+ end
13
+ attr_reader :code, :message
14
+ alias :to_s :message
15
+
16
+ def to_s
17
+ end
18
+ end
19
+
20
+ # The type returned by a Command class when it is executed.
21
+ class Result
22
+ def initialize(value, errors=[])
23
+ @errors = errors
24
+ @value = value
25
+ end
26
+ attr_reader :errors, :value
27
+
28
+ def error_codes
29
+ errors.map(&:code)
30
+ end
31
+
32
+ def error_messages
33
+ errors.map(&:message)
34
+ end
35
+
36
+ def failed?
37
+ !success?
38
+ end
39
+ alias :error? :failed?
40
+
41
+ def success?
42
+ errors.empty?
43
+ end
44
+ end
45
+
46
+ @@convertors = {self => {}}
47
+ @@parameters = {self => {}}
48
+ @@validators = {self => {}}
49
+
50
+ def initialize(parameters={})
51
+ @convertors = Command.convertors_for(self.class)
52
+ @errors = []
53
+ @parameters = {}.merge(parameters)
54
+ @settings = Command.parameters(self.class)
55
+ @validators = Command.validators_for(self.class)
56
+ end
57
+ attr_reader :convertors, :errors, :parameters, :settings, :validators
58
+
59
+ # Test whether a given value is convertible for a named parameter. This will
60
+ # return true if the parameter is expected and either has no type specified
61
+ # or the value given can be converted to the parameters specified type.
62
+ def convertible?(name, value)
63
+ result = false
64
+ if expects?(name)
65
+ result = true
66
+ settings = @settings[name]
67
+ if settings.type
68
+ result = get_convertor(settings.type).convertible?(value)
69
+ end
70
+ end
71
+ result
72
+ end
73
+
74
+ # Register an error with the execution of the current Command.
75
+ def error(message)
76
+ @errors << Error.new(message)
77
+ end
78
+
79
+ # Invokes the #perform() method if and only if the Command instance tests as
80
+ # valid. This method should be the one invoked to run a Command instance.
81
+ def execute
82
+ @errors = []
83
+ value = nil
84
+ value = perform if valid?
85
+ Result.new(value, errors)
86
+ end
87
+
88
+ # Returns a list of the expected parameters configured for a Command
89
+ # instance.
90
+ def expected_parameter_names
91
+ @settings ? @settings.values.map(&:name) : []
92
+ end
93
+
94
+ # Tests whether a parameter name is among the parameters specified for the
95
+ # Command instance.
96
+ def expects?(parameter)
97
+ expected_parameter_names.include?(parameter)
98
+ end
99
+
100
+ # Retrieve the value for a named parameter. The value will be run through an
101
+ # applicable converted prior to being returned. An exception will be raised
102
+ # if conversion fails. If the parameter is optional and has not be specified
103
+ # then conversion will not be attempted and nil will be returned.
104
+ def get_parameter_value(name)
105
+ if expects?(name)
106
+ settings = settings_for(name)
107
+ if settings[:required] && !provided?(name)
108
+ raise ParameterError.new("A value has not been provided for the '#{name}' parameter.", name)
109
+ end
110
+
111
+ if settings[:required] || provided?(name)
112
+ value = get_raw_parameter_value(name)
113
+ convertor = get_convertor(settings.type)
114
+ if !convertor.convertible?(value)
115
+ raise ParameterError.new("The value of the '#{name}' parameter cannot be converted to the '#{settings.type}' type.", name)
116
+ end
117
+ convertor.convert(value)
118
+ else
119
+ nil
120
+ end
121
+ else
122
+ raise ParameterError.new("Unknown parameter '#{name}' requested from a '#{self.class.name}' command instance.")
123
+ end
124
+ end
125
+
126
+ # Fetches a name convertor from the list for the Command instance, raises
127
+ # an exception if one cannot be found.
128
+ def get_convertor(type)
129
+ if !has_convertor?(type)
130
+ raise CommandError.new("Unable to locate the '#{type}' parameter convertor.")
131
+ end
132
+ @convertors[type]
133
+ end
134
+
135
+ # Fetches the raw, unaltered value specified for a name parameter to the
136
+ # Command instance. Returns nil if the specified parameter has not been
137
+ # given an explicit value. Raises an exception if an unknown parameter is
138
+ # specified.
139
+ def get_raw_parameter_value(name)
140
+ raise ParameterError.new("Unknown parameter '#{name}' requested in command.", name) if !expects?(name)
141
+ @parameters[name]
142
+ end
143
+
144
+ # This method tests whether a named convertor is available to a Command
145
+ # instance.
146
+ def has_convertor?(name)
147
+ @convertors.include?(name)
148
+ end
149
+
150
+ # An implementation of the #method_missing method for the Command class that
151
+ # checks whether a parameter is being requested and, if so, returns it's value
152
+ # or delegates handling to the parent class implementation.
153
+ def method_missing(name, *arguments, &block)
154
+ if expects?(name)
155
+ get_parameter_value(name)
156
+ else
157
+ super
158
+ end
159
+ end
160
+
161
+ # Returns a list of the names of the commands optional parameters.
162
+ def optional_parameter_names
163
+ settings.values.filter {|p| !p.required}.map(&:name)
164
+ end
165
+
166
+ # Returns a list of the names of the parameters specified to the Command
167
+ # instance.
168
+ def parameter_names
169
+ @settings.keys
170
+ end
171
+
172
+ # Derived command classes should override this method to do the work for the
173
+ # command. This method will only get invoked if the command is valid. This
174
+ # default implementation raises an exception.
175
+ def perform
176
+ raise CommandError.new("The #{self.class.name} command class has not overridden the #perform() method.")
177
+ end
178
+
179
+ # This method checks whether a name parameter is among those provided to a
180
+ # Command instance.
181
+ def provided?(name)
182
+ @parameters.include?(name)
183
+ end
184
+
185
+ # Returns a list of the names of the commands required parameters. Note a
186
+ # required parameter must have a value specified for it when the command
187
+ # is executed.
188
+ def required_parameter_names
189
+ settings.values.filter {|p| p.required}.map(&:name)
190
+ end
191
+
192
+ # Retrieves the parameter settings for a named parameter. Raises an
193
+ # exception if an unknown parameter is specified.
194
+ def settings_for(name)
195
+ raise ParameterError("Unknown parameter '#{name}' requested in command.", name) if !expects?(name)
196
+ entry = @settings.find {|entry| entry[1].name == name}
197
+ entry ? entry[1] : nil
198
+ end
199
+
200
+ # Performs validation of the parameters passed to a command. Deriving classes
201
+ # should ensure this method is invoked in any custom #validate method their
202
+ # class provides.
203
+ def validate
204
+ @settings.values.each do |parameter|
205
+ if provided?(parameter.name)
206
+ if parameter.type
207
+ # Check conversion.
208
+ if has_convertor?(parameter.type)
209
+ convertor = get_convertor(parameter.type)
210
+ if !convertor.convertible?(get_raw_parameter_value(parameter.name))
211
+ error("The value of the '#{parameter.name}' parameter cannot be converted to the '#{parameter.type}' type.")
212
+ end
213
+ else
214
+ error("Invalid type '#{parameter.type}' specified for the '#{parameter.name}' parameter.")
215
+ end
216
+ end
217
+
218
+ # Run validations.
219
+ if convertible?(parameter.name, get_raw_parameter_value(parameter.name))
220
+ value = get_parameter_value(parameter.name)
221
+ validations_for(parameter.name).each do |validation|
222
+ self.instance_exec(parameter.name, value, &validation)
223
+ end
224
+ else
225
+ error("The value of the '#{parameter.name}' parameter cannot be converted to the '#{parameter.type}' type.")
226
+ end
227
+ else
228
+ if parameter.required
229
+ error("No value specified for the '#{parameter.name}' required parameter.")
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ # Invokes the validate command and then checks that there are no errors
236
+ # registered for the command.
237
+ def valid?
238
+ @errors = []
239
+ validate
240
+ @errors.empty?
241
+ end
242
+
243
+ # Returns a list of the validators that apply to a named parameter. This
244
+ # will be a combination of validators explicitly declared on the parameter
245
+ # and class validators with the same name as the parameter. The method
246
+ # raises an exception if given the name of a parameter that the Command
247
+ # instance does not expect. It can also raise an exception if a parameter
248
+ # has an unknown validator specified for it.
249
+ def validations_for(name)
250
+ if !expects?(name)
251
+ raise ParameterError.new("Validators requested for unknown parameter '#{name}'.", name)
252
+ end
253
+ settings = @settings[name]
254
+ names = []
255
+ names << name if @validators.include?(name)
256
+ names = names.concat(settings.validations) if settings.validations
257
+ names.uniq.map do |key|
258
+ if !@validators.include?(key)
259
+ raise ParameterError.new("Unknown validation '#{key}' requested for the '#{name}' parameter.", name)
260
+ end
261
+ @validators[key]
262
+ end
263
+ end
264
+
265
+ # Registers a convertor for a Command class. A convertor is any class that
266
+ # can be constructed using a default constructor and responds to the
267
+ # #convertible?() and #convert() methods. Both of these methods take a
268
+ # single parameter which is the value to undergo conversion. The
269
+ # #convertible?() method returns true if it's possible to convert the value
270
+ # to the convertors output type. The #convert() method performs the actual
271
+ # conversion, returning the result.
272
+ def self.add_convertor(name, convertor_class)
273
+ @@convertors[self] = {} if !@@convertors.include?(self)
274
+ if @@convertors[self].include?(name)
275
+ raise CommandError.new("Duplicate convertor '#{name}' specified for the #{self.name} class.")
276
+ end
277
+
278
+ @@convertors[self][name] = convertor_class
279
+ end
280
+
281
+ # Registers a validator for a Command class. A validator has to be registered
282
+ # with a block that will be invoked for the relevant parameters. This block
283
+ # should take 3 parameters. The first is the command object being executed.
284
+ # The second is the name of the parameter being validated. The third is the
285
+ # value of the parameter being validated. Validators can register errors by
286
+ # invoking the #error() method on the command they are passed.
287
+ def self.add_validator(name, &block)
288
+ @@validators[self] = {} if !@@validators.include?(self)
289
+ if @@validators[self].include?(name)
290
+ raise CommandError.new("Duplicate validator '#{name}' specified for the #{self.name} class.")
291
+ end
292
+
293
+ if !block
294
+ raise CommandError.new("No block specified for the '#{name}' validator in the #{self.name} class.")
295
+ end
296
+
297
+ @@validators[self][name] = block
298
+ end
299
+
300
+ # This method scans the class hierarchy for a Command instance and assembles
301
+ # a list of the registered convertors for it. Convertors registered in classes
302
+ # lower in the hierarchy (i.e. derived classes) override those registered in
303
+ # parent classes.
304
+ def self.convertors_for(command_class)
305
+ hierarchy = [command_class]
306
+ while !hierarchy.last.superclass.nil?
307
+ hierarchy << hierarchy.last.superclass
308
+ end
309
+
310
+ convertors = {}
311
+ hierarchy.reverse.each do |c|
312
+ convertors.merge!(@@convertors[c]) if @@convertors.include?(c)
313
+ end
314
+ convertors.inject({}) {|list, entry| list[entry[0]] = entry[1].new; list}
315
+ end
316
+
317
+ # Registers an optional parameter for the command. See the #parameter() method
318
+ # for details of the parameters this method accepts.
319
+ def self.optional(name, settings={}, &block)
320
+ parameter(name, {}.merge(settings, {required: false}), &block)
321
+ end
322
+
323
+ # Register a new parameter for a Command class. The first method parameter
324
+ # specifies the new parameters name. This can be followed by a Hash of
325
+ # settings value for the parameter. All keys in this Hash should be symbols
326
+ # and the following keys are currently recognised - :required, :types and
327
+ # :validators. You can also register a block for a parameter. This block
328
+ # will be invoked with the raw parameter value and the return value from this
329
+ # block will become the actual parameter value used.
330
+ def self.parameter(name, settings={}, &block)
331
+ if self.method_defined?(name)
332
+ raise ParameterError.new("The '#{name}' parameter clashes with an existing class method.", name)
333
+ end
334
+ @@parameters[self] = {} if !@@parameters.include?(self)
335
+ @@parameters[self][name] = OpenStruct.new({}.merge(settings, {name: name, block: block}))
336
+ end
337
+
338
+ # Fetches the parameter list registered for a specific Command class
339
+ # instance.
340
+ def self.parameters(command_class)
341
+ @@parameters[command_class]
342
+ end
343
+
344
+ # Registers an optional parameter for the command. See the #parameter() method
345
+ # for details of the parameters this method accepts.
346
+ def self.required(name, settings={}, &block)
347
+ parameter(name, {}.merge(settings, {required: true}), &block)
348
+ end
349
+
350
+ # A synomym for the #add_validator() method that is intended for use with
351
+ # a validator that matches a parameter name.
352
+ def self.validate(name, &block)
353
+ add_validator(name, &block)
354
+ end
355
+
356
+ # This method scans the class hierarchy for a Command instance and assembles
357
+ # a list of the registered validators for it. Validators registered in classes
358
+ # lower in the hierarchy (i.e. derived classes) override those registered in
359
+ # parent classes.
360
+ def self.validators_for(command_class)
361
+ hierarchy = [command_class]
362
+ while !hierarchy.last.superclass.nil?
363
+ hierarchy << hierarchy.last.superclass
364
+ end
365
+
366
+ validators = {}
367
+ hierarchy.reverse.each do |c|
368
+ validators.merge!(@@validators[c]) if @@validators.include?(c)
369
+ end
370
+ validators
371
+ end
372
+
373
+ # ----------------------------------------------------------------------------
374
+ # Add default library validators
375
+ # ----------------------------------------------------------------------------
376
+ add_validator(:not_blank) do |name, value|
377
+ if [nil, ""].include?("#{value}".strip)
378
+ error("Blank value specified for the '#{name}' parameter.")
379
+ end
380
+ end
381
+
382
+ add_validator(:not_nil) do |name, value|
383
+ error("Nil value specified for the '#{name}' parameter.") if value.nil?
384
+ end
385
+
386
+ # ----------------------------------------------------------------------------
387
+ # Add default library convertors
388
+ # ----------------------------------------------------------------------------
389
+ add_convertor :boolean, Chieftain::BooleanConvertor
390
+ add_convertor :float, Chieftain::FloatConvertor
391
+ add_convertor :integer, Chieftain::IntegerConvertor
392
+ add_convertor :string, Chieftain::StringConvertor
393
+ end
394
+ end
@@ -0,0 +1,50 @@
1
+ module Chieftain
2
+ # A convertor for boolean values.
3
+ class BooleanConvertor
4
+ VALID_TRUE_VALUES = ["1", "on", "true", "y", "yes"]
5
+ VALID_FALSE_VALUES = ["0", "false", "n", "no", "off"]
6
+ VALID_VALUES = VALID_FALSE_VALUES + VALID_TRUE_VALUES
7
+
8
+ def convertible?(value)
9
+ [FalseClass, TrueClass].include?(value.class) ||
10
+ VALID_VALUES.include?(value.to_s.downcase)
11
+ end
12
+
13
+ def convert(value)
14
+ VALID_TRUE_VALUES.include?(value.to_s.downcase)
15
+ end
16
+ end
17
+
18
+ # A convertor floating point values.
19
+ class FloatConvertor
20
+ def convertible?(value)
21
+ value.to_f.to_s == "#{value}"
22
+ end
23
+
24
+ def convert(value)
25
+ value.to_f
26
+ end
27
+ end
28
+
29
+ # A convertor for integer values.
30
+ class IntegerConvertor
31
+ def convertible?(value)
32
+ value.to_i.to_s == "#{value}"
33
+ end
34
+
35
+ def convert(value)
36
+ value.to_i
37
+ end
38
+ end
39
+
40
+ # A convertor for string values.
41
+ class StringConvertor
42
+ def convertible?(value)
43
+ true
44
+ end
45
+
46
+ def convert(value)
47
+ value.to_s
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,17 @@
1
+ module Chieftain
2
+ # The root exception class used by the Chieftain class hierarchy.
3
+ class CommandError < StandardError
4
+ def initialize(message)
5
+ super(message)
6
+ end
7
+ end
8
+
9
+ # A command error class relating specifically to a parameter.
10
+ class ParameterError < CommandError
11
+ def initialize(message, parameter)
12
+ super(message)
13
+ @parameter = parameter
14
+ end
15
+ attr_reader :parameter
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chieftain
4
+ VERSION = "0.1.0"
5
+ end
data/lib/chieftain.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chieftain/version"
4
+ require_relative "chieftain/exceptions"
5
+ require_relative "chieftain/convertors"
6
+ require_relative "chieftain/command"
data/sig/chieftain.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Chieftain
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chieftain
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter Wood
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-11-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: An implementation of the command design pattern that attempts to simplify
14
+ usage by enchancing the offering making use of the facilities offered by the Ruby
15
+ language.
16
+ email:
17
+ - pw0470@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".rspec"
23
+ - Gemfile
24
+ - Gemfile.lock
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - lib/chieftain.rb
29
+ - lib/chieftain/command.rb
30
+ - lib/chieftain/convertors.rb
31
+ - lib/chieftain/exceptions.rb
32
+ - lib/chieftain/version.rb
33
+ - sig/chieftain.rbs
34
+ homepage: https://github.com/free-beer/chieftain
35
+ licenses:
36
+ - Apache-2.0
37
+ metadata:
38
+ allowed_push_host: https://rubygems.org
39
+ homepage_uri: https://github.com/free-beer/chieftain
40
+ source_code_uri: https://github.com/free-beer/chieftain
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 2.6.0
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.3.7
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: An implementation of the Command design pattern in Ruby.
60
+ test_files: []