command_model 2.0.0 → 2.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.
@@ -0,0 +1,52 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ command_model (2.0.1)
5
+ activemodel (> 6.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (6.1.7.7)
11
+ activesupport (= 6.1.7.7)
12
+ activesupport (6.1.7.7)
13
+ concurrent-ruby (~> 1.0, >= 1.0.2)
14
+ i18n (>= 1.6, < 2)
15
+ minitest (>= 5.1)
16
+ tzinfo (~> 2.0)
17
+ zeitwerk (~> 2.3)
18
+ concurrent-ruby (1.2.3)
19
+ diff-lcs (1.5.1)
20
+ i18n (1.14.4)
21
+ concurrent-ruby (~> 1.0)
22
+ minitest (5.22.3)
23
+ rake (13.1.0)
24
+ rspec (3.13.0)
25
+ rspec-core (~> 3.13.0)
26
+ rspec-expectations (~> 3.13.0)
27
+ rspec-mocks (~> 3.13.0)
28
+ rspec-core (3.13.0)
29
+ rspec-support (~> 3.13.0)
30
+ rspec-expectations (3.13.0)
31
+ diff-lcs (>= 1.2.0, < 2.0)
32
+ rspec-support (~> 3.13.0)
33
+ rspec-mocks (3.13.0)
34
+ diff-lcs (>= 1.2.0, < 2.0)
35
+ rspec-support (~> 3.13.0)
36
+ rspec-support (3.13.1)
37
+ tzinfo (2.0.6)
38
+ concurrent-ruby (~> 1.0)
39
+ zeitwerk (2.6.13)
40
+
41
+ PLATFORMS
42
+ arm64-darwin-23
43
+ x86_64-linux
44
+
45
+ DEPENDENCIES
46
+ activemodel (~> 6.1.0)
47
+ command_model!
48
+ rake (~> 13.1.0)
49
+ rspec (~> 3.13.0)
50
+
51
+ BUNDLED WITH
52
+ 2.4.3
@@ -1,5 +1,5 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem "activemodel", "~> 5.1.0"
3
+ gem "activemodel", "~> 7.0.0"
4
4
 
5
5
  gemspec :path=>"../"
@@ -0,0 +1,50 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ command_model (2.0.1)
5
+ activemodel (> 6.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (7.0.4.3)
11
+ activesupport (= 7.0.4.3)
12
+ activesupport (7.0.4.3)
13
+ concurrent-ruby (~> 1.0, >= 1.0.2)
14
+ i18n (>= 1.6, < 2)
15
+ minitest (>= 5.1)
16
+ tzinfo (~> 2.0)
17
+ concurrent-ruby (1.2.3)
18
+ diff-lcs (1.5.1)
19
+ i18n (1.14.4)
20
+ concurrent-ruby (~> 1.0)
21
+ minitest (5.22.3)
22
+ rake (13.1.0)
23
+ rspec (3.13.0)
24
+ rspec-core (~> 3.13.0)
25
+ rspec-expectations (~> 3.13.0)
26
+ rspec-mocks (~> 3.13.0)
27
+ rspec-core (3.13.0)
28
+ rspec-support (~> 3.13.0)
29
+ rspec-expectations (3.13.0)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.13.0)
32
+ rspec-mocks (3.13.0)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.13.0)
35
+ rspec-support (3.13.1)
36
+ tzinfo (2.0.6)
37
+ concurrent-ruby (~> 1.0)
38
+
39
+ PLATFORMS
40
+ arm64-darwin-23
41
+ x86_64-linux
42
+
43
+ DEPENDENCIES
44
+ activemodel (~> 7.0.0)
45
+ command_model!
46
+ rake (~> 13.1.0)
47
+ rspec (~> 3.13.0)
48
+
49
+ BUNDLED WITH
50
+ 2.4.3
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activemodel", "~> 7.1.0"
4
+
5
+ gemspec :path=>"../"
@@ -0,0 +1,60 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ command_model (2.0.1)
5
+ activemodel (> 6.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (7.1.3.2)
11
+ activesupport (= 7.1.3.2)
12
+ activesupport (7.1.3.2)
13
+ base64
14
+ bigdecimal
15
+ concurrent-ruby (~> 1.0, >= 1.0.2)
16
+ connection_pool (>= 2.2.5)
17
+ drb
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ mutex_m
21
+ tzinfo (~> 2.0)
22
+ base64 (0.2.0)
23
+ bigdecimal (3.1.7)
24
+ concurrent-ruby (1.2.3)
25
+ connection_pool (2.4.1)
26
+ diff-lcs (1.5.1)
27
+ drb (2.2.1)
28
+ i18n (1.14.4)
29
+ concurrent-ruby (~> 1.0)
30
+ minitest (5.22.3)
31
+ mutex_m (0.2.0)
32
+ rake (13.1.0)
33
+ rspec (3.13.0)
34
+ rspec-core (~> 3.13.0)
35
+ rspec-expectations (~> 3.13.0)
36
+ rspec-mocks (~> 3.13.0)
37
+ rspec-core (3.13.0)
38
+ rspec-support (~> 3.13.0)
39
+ rspec-expectations (3.13.0)
40
+ diff-lcs (>= 1.2.0, < 2.0)
41
+ rspec-support (~> 3.13.0)
42
+ rspec-mocks (3.13.0)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.13.0)
45
+ rspec-support (3.13.1)
46
+ tzinfo (2.0.6)
47
+ concurrent-ruby (~> 1.0)
48
+
49
+ PLATFORMS
50
+ arm64-darwin-23
51
+ x86_64-linux
52
+
53
+ DEPENDENCIES
54
+ activemodel (~> 7.1.0)
55
+ command_model!
56
+ rake (~> 13.1.0)
57
+ rspec (~> 3.13.0)
58
+
59
+ BUNDLED WITH
60
+ 2.4.3
@@ -61,7 +61,7 @@ module CommandModel
61
61
  return nil if value.blank?
62
62
  return value if value.kind_of? Date
63
63
  value = value.to_s
64
- if value =~ /\A(\d\d\d\d)-(\d\d)-(\d\d)\z/
64
+ if value =~ /\A(\d{4,5})-(\d\d)-(\d\d)\z/
65
65
  ::Date.civil($1.to_i, $2.to_i, $3.to_i)
66
66
  else
67
67
  ::Date.strptime(value, "%m/%d/%Y")
@@ -1,18 +1,15 @@
1
1
  module CommandModel
2
- class TypecastError < StandardError
3
- attr_reader :original_error
4
-
5
- def initialize(original_error)
6
- @original_error = original_error
7
- end
8
- end
9
-
10
2
  class Model
11
3
  include ActiveModel::Validations
12
4
  include ActiveModel::Conversion
13
5
  extend ActiveModel::Naming
14
6
 
15
- Parameter = Struct.new(:name, :converters, :validations)
7
+ def self.inherited(subclass)
8
+ subclass.instance_variable_set :@parameters, parameters.dup.freeze
9
+ subclass.instance_variable_set :@dependencies, dependencies.dup.freeze
10
+ end
11
+
12
+ Parameter = Data.define(:name, :converters, :validations)
16
13
 
17
14
  # Parameter requires one or more attributes as its first parameter(s).
18
15
  # It accepts an options hash as its last parameter.
@@ -34,11 +31,12 @@ module CommandModel
34
31
  # parameter :height, :weight,
35
32
  # convert: [CommandModel::Convert::StringMutator.new { |s| s.gsub(",", "")}, :integer],
36
33
  # presence: true,
37
- # numericality: { :greater_than_or_equal_to => 0 }
34
+ # numericality: { greater_than_or_equal_to: 0 }
38
35
  def self.parameter(*args)
39
36
  options = args.last.kind_of?(Hash) ? args.pop.clone : {}
40
37
  converters = options.delete(:convert)
41
38
 
39
+ @parameters ||= [].freeze
42
40
  args.each do |name|
43
41
  attr_reader name
44
42
 
@@ -48,13 +46,13 @@ module CommandModel
48
46
  attr_writer name
49
47
  end
50
48
  validates name, options.clone if options.present? # clone options because validates mutates the hash :(
51
- parameters.push Parameter.new name, converters, options
49
+ @parameters = (@parameters + [Parameter.new(name, converters, options)]).freeze
52
50
  end
53
51
  end
54
52
 
55
53
  # Returns array of all parameters defined for class
56
54
  def self.parameters
57
- @parameters ||= []
55
+ @parameters ||= [].freeze
58
56
  end
59
57
 
60
58
  def self.attr_type_converting_writer(name, converters) #:nodoc
@@ -90,6 +88,38 @@ module CommandModel
90
88
  end
91
89
  end
92
90
 
91
+ Dependency = Data.define(:name, :default, :allow_blank)
92
+
93
+ # Dependency requires one or more attributes as its first parameter(s). A dependency is something that is required
94
+ # for the command to execute that is not user supplied input. For example, a database connection, a logger, or the
95
+ # current user.
96
+ #
97
+ # ==== Keyword Arguments
98
+ #
99
+ # * default - An object that will be used as the default value for the dependency or a callable object that will be
100
+ # called to get the default value.
101
+ # * allow_blank - If true, the dependency can be nil or blank. If false, the dependency must be present.
102
+ #
103
+ # ==== Examples
104
+ #
105
+ # dependency :current_user
106
+ # dependency :stdout, default: -> { $stdout }
107
+ def self.dependency(*names, default: nil, allow_blank: false)
108
+ @dependencies ||= [].freeze
109
+ names.each do |name|
110
+ name = name.to_sym
111
+ attr_reader name
112
+ private attr_writer name
113
+ default_callable = default.respond_to?(:call) ? default : -> { default }
114
+ @dependencies = (@dependencies + [Dependency.new(name, default_callable, allow_blank)]).freeze
115
+ end
116
+ end
117
+
118
+ # Returns array of all dependencies defined for class.
119
+ def self.dependencies
120
+ @dependencies ||= [].freeze
121
+ end
122
+
93
123
  # Executes a block of code if the command model is valid.
94
124
  #
95
125
  # Accepts either a command model or a hash of attributes with which to
@@ -97,18 +127,19 @@ module CommandModel
97
127
  #
98
128
  # ==== Examples
99
129
  #
100
- # RenameUserCommand.execute(:login => "john") do |command|
130
+ # RenameUserCommand.execute(login: "john") do |command|
101
131
  # if allowed_to_rename_user?
102
132
  # self.login = command.login
103
133
  # else
104
134
  # command.errors.add :base, "not allowed to rename"
105
135
  # end
106
136
  # end
107
- def self.execute(attributes_or_command, &block)
137
+ def self.execute(attributes_or_command, dependencies={}, &block)
108
138
  command = if attributes_or_command.kind_of? self
139
+ raise ArgumentError, "cannot pass dependencies with already initialized command" if dependencies.present?
109
140
  attributes_or_command
110
141
  else
111
- new(attributes_or_command)
142
+ new(attributes_or_command, dependencies)
112
143
  end
113
144
 
114
145
  command.call &block
@@ -137,9 +168,23 @@ module CommandModel
137
168
  # Accepts a parameters hash or another of the same class. If another
138
169
  # instance of the same class is passed in then the parameters are copied
139
170
  # to the new object.
140
- def initialize(parameters={})
171
+ def initialize(parameters={}, dependencies={})
141
172
  @type_conversion_errors = {}
142
173
  set_parameters parameters
174
+
175
+ dependencies = dependencies.symbolize_keys
176
+ self.class.dependencies.each do |dependency|
177
+ value = dependencies.fetch(dependency.name, dependency.default.call)
178
+ if value.blank? && !dependency.allow_blank
179
+ raise ArgumentError, "Dependency #{dependency.name} cannot be blank"
180
+ end
181
+ self.send "#{dependency.name}=", dependencies.fetch(dependency.name, dependency.default.call)
182
+ end
183
+
184
+ unknown_dependencies = dependencies.keys - self.class.dependencies.map(&:name)
185
+ if unknown_dependencies.present?
186
+ raise ArgumentError, "Unknown dependencies: #{bad_dependencies.join(", ")}"
187
+ end
143
188
  end
144
189
 
145
190
  # Executes the command by calling the method +execute+ if the validations
@@ -1,3 +1,3 @@
1
1
  module CommandModel
2
- VERSION = "2.0.0"
2
+ VERSION = "2.1.0"
3
3
  end
data/spec/convert_spec.rb CHANGED
@@ -30,7 +30,7 @@ describe "CommandModel::Convert" do
30
30
  expect(subject.("")).to eq(nil)
31
31
  end
32
32
 
33
- it "raises TypecastError when invalid string" do
33
+ it "raises ConvertError when invalid string" do
34
34
  expect { subject.("asdf") }.to raise_error(CommandModel::Convert::ConvertError)
35
35
  expect { subject.("0.1") }.to raise_error(CommandModel::Convert::ConvertError)
36
36
  end
@@ -52,7 +52,7 @@ describe "CommandModel::Convert" do
52
52
  expect(subject.("")).to eq(nil)
53
53
  end
54
54
 
55
- it "raises TypecastError when invalid string" do
55
+ it "raises ConvertError when invalid string" do
56
56
  expect { subject.("asdf") }.to raise_error(CommandModel::Convert::ConvertError)
57
57
  end
58
58
  end
@@ -81,7 +81,7 @@ describe "CommandModel::Convert" do
81
81
  expect(subject.("")).to eq(nil)
82
82
  end
83
83
 
84
- it "raises TypecastError when invalid string" do
84
+ it "raises ConvertError when invalid string" do
85
85
  expect { subject.("asdf") }.to raise_error(CommandModel::Convert::ConvertError)
86
86
  end
87
87
  end
@@ -93,6 +93,7 @@ describe "CommandModel::Convert" do
93
93
  expect(subject.("01/01/2000")).to eq(Date.civil(2000,1,1))
94
94
  expect(subject.("1/1/2000")).to eq(Date.civil(2000,1,1))
95
95
  expect(subject.("2000-01-01")).to eq(Date.civil(2000,1,1))
96
+ expect(subject.("29000-01-01")).to eq(Date.civil(29000,1,1))
96
97
  end
97
98
 
98
99
  it "returns existing date unchanged" do
@@ -108,7 +109,7 @@ describe "CommandModel::Convert" do
108
109
  expect(subject.("")).to eq(nil)
109
110
  end
110
111
 
111
- it "raises TypecastError when invalid string" do
112
+ it "raises ConvertError when invalid string" do
112
113
  expect { subject.("asdf") }.to raise_error(CommandModel::Convert::ConvertError)
113
114
  expect { subject.("3/50/1290") }.to raise_error(CommandModel::Convert::ConvertError)
114
115
  end
data/spec/model_spec.rb CHANGED
@@ -6,7 +6,7 @@ class ExampleCommand < CommandModel::Model
6
6
  end
7
7
 
8
8
  describe CommandModel::Model do
9
- let(:example_command) { ExampleCommand.new :name => "John" }
9
+ let(:example_command) { ExampleCommand.new name: "John" }
10
10
  let(:invalid_example_command) { ExampleCommand.new }
11
11
 
12
12
  describe "self.parameter" do
@@ -38,7 +38,7 @@ describe CommandModel::Model do
38
38
  end
39
39
 
40
40
  it "accepts multiple attributes with convert" do
41
- klass.parameter :foo, :bar, :convert => :integer
41
+ klass.parameter :foo, :bar, convert: :integer
42
42
  expect(klass.new.methods).to include(:foo)
43
43
  expect(klass.new.methods).to include(:foo=)
44
44
  expect(klass.new.methods).to include(:bar)
@@ -46,7 +46,7 @@ describe CommandModel::Model do
46
46
  end
47
47
 
48
48
  it "accepts multiple attributes with validation" do
49
- klass.parameter :foo, :bar, :presence => true
49
+ klass.parameter :foo, :bar, presence: true
50
50
  expect(klass.new.methods).to include(:foo)
51
51
  expect(klass.new.methods).to include(:foo=)
52
52
  expect(klass.new.methods).to include(:bar)
@@ -72,6 +72,27 @@ describe CommandModel::Model do
72
72
  expect(instance).to_not be_valid
73
73
  expect(instance.errors[:name]).to be_present
74
74
  end
75
+
76
+ it "works when model is inherited" do
77
+ klass.parameter :foo
78
+ expect(klass.parameters.map(&:name)).to eq([:foo])
79
+
80
+ klass_b = Class.new(klass)
81
+ expect(klass_b.parameters.map(&:name)).to eq([:foo])
82
+ klass_b.parameter :bar
83
+ expect(klass.parameters.map(&:name)).to eq([:foo])
84
+ expect(klass_b.parameters.map(&:name)).to eq([:foo, :bar])
85
+
86
+ klass_c = Class.new(klass_b)
87
+ expect(klass.parameters.map(&:name)).to eq([:foo])
88
+ expect(klass_b.parameters.map(&:name)).to eq([:foo, :bar])
89
+ expect(klass_c.parameters.map(&:name)).to eq([:foo, :bar])
90
+
91
+ klass_c.parameter :baz
92
+ expect(klass.parameters.map(&:name)).to eq([:foo])
93
+ expect(klass_b.parameters.map(&:name)).to eq([:foo, :bar])
94
+ expect(klass_c.parameters.map(&:name)).to eq([:foo, :bar, :baz])
95
+ end
75
96
  end
76
97
 
77
98
  describe "self.parameters" do
@@ -89,13 +110,65 @@ describe CommandModel::Model do
89
110
  end
90
111
  end
91
112
 
113
+ describe "self.dependency" do
114
+ let(:klass) { Class.new(CommandModel::Model) }
115
+
116
+ it "creates an attribute reader" do
117
+ klass.dependency :foo, allow_blank: true
118
+ expect(klass.new.methods).to include(:foo)
119
+ end
120
+
121
+ it "accepts multiple attributes" do
122
+ klass.dependency :foo, :bar, allow_blank: true
123
+ expect(klass.new.methods).to include(:foo)
124
+ expect(klass.new.methods).to include(:bar)
125
+ end
126
+
127
+ it "accepts multiple attributes with default" do
128
+ klass.dependency :foo, :bar, default: -> { "baz" }
129
+ expect(klass.new.methods).to include(:foo)
130
+ expect(klass.new.methods).to include(:bar)
131
+ end
132
+
133
+ it "works when model is inherited" do
134
+ klass.dependency :foo
135
+ expect(klass.dependencies.map(&:name)).to eq([:foo])
136
+
137
+ klass_b = Class.new(klass)
138
+ expect(klass_b.dependencies.map(&:name)).to eq([:foo])
139
+ klass_b.dependency :bar
140
+ expect(klass.dependencies.map(&:name)).to eq([:foo])
141
+ expect(klass_b.dependencies.map(&:name)).to eq([:foo, :bar])
142
+
143
+ klass_c = Class.new(klass_b)
144
+ expect(klass.dependencies.map(&:name)).to eq([:foo])
145
+ expect(klass_b.dependencies.map(&:name)).to eq([:foo, :bar])
146
+ expect(klass_c.dependencies.map(&:name)).to eq([:foo, :bar])
147
+
148
+ klass_c.dependency :baz
149
+ expect(klass.dependencies.map(&:name)).to eq([:foo])
150
+ expect(klass_b.dependencies.map(&:name)).to eq([:foo, :bar])
151
+ expect(klass_c.dependencies.map(&:name)).to eq([:foo, :bar, :baz])
152
+ end
153
+ end
154
+
155
+ describe "self.dependencies" do
156
+ it "returns all dependencies in class" do
157
+ klass = Class.new(CommandModel::Model)
158
+ klass.dependency :foo
159
+ klass.dependency :bar
160
+
161
+ expect(klass.dependencies.map(&:name)).to eq([:foo, :bar])
162
+ end
163
+ end
164
+
92
165
  describe "self.execute" do
93
166
  it "accepts object of same kind and returns it" do
94
167
  expect(ExampleCommand.execute(example_command) {}).to eq(example_command)
95
168
  end
96
169
 
97
170
  it "accepts attributes, creates object, and returns it" do
98
- c = ExampleCommand.execute(:name => "John") {}
171
+ c = ExampleCommand.execute(name: "John") {}
99
172
  expect(c).to be_kind_of(ExampleCommand)
100
173
  expect(c.name).to eq("John")
101
174
  end
@@ -129,6 +202,25 @@ describe CommandModel::Model do
129
202
 
130
203
  expect(example_command).to_not be_success
131
204
  end
205
+
206
+ it "uses default dependencies when not provided" do
207
+ klass = Class.new(CommandModel::Model)
208
+ klass.dependency :stdout, default: -> { $stdout }
209
+ klass.parameter :name
210
+ m = klass.execute(name: "John")
211
+ expect(m.stdout).to eq($stdout)
212
+ expect(m.execution_attempted?).to eq(true)
213
+ end
214
+
215
+ it "accepts dependencies from arguments" do
216
+ klass = Class.new(CommandModel::Model)
217
+ klass.dependency :stdout, default: -> { $stdout }
218
+ klass.parameter :name
219
+ writer = StringIO.new
220
+ m = klass.execute({name: "John"}, stdout: writer)
221
+ expect(m.stdout).to eq(writer)
222
+ expect(m.execution_attempted?).to eq(true)
223
+ end
132
224
  end
133
225
 
134
226
  describe "self.success" do
@@ -151,15 +243,46 @@ describe CommandModel::Model do
151
243
 
152
244
  describe "initialize" do
153
245
  it "assigns parameters from hash" do
154
- m = ExampleCommand.new :name => "John"
246
+ m = ExampleCommand.new name: "John"
155
247
  expect(m.name).to eq("John")
156
248
  end
157
249
 
158
250
  it "assigns parameters from other CommandModel" do
159
- other = ExampleCommand.new :name => "John"
251
+ other = ExampleCommand.new name: "John"
160
252
  m = ExampleCommand.new other
161
253
  expect(m.name).to eq(other.name)
162
254
  end
255
+
256
+ it "assigns default dependencies when not provided" do
257
+ klass = Class.new(CommandModel::Model)
258
+ klass.dependency :stdout, default: -> { $stdout }
259
+ klass.parameter :name
260
+ m = klass.new name: "John"
261
+ expect(m.stdout).to eq($stdout)
262
+ end
263
+
264
+ it "assigns dependencies from arguments" do
265
+ klass = Class.new(CommandModel::Model)
266
+ klass.dependency :stdout, default: -> { $stdout }
267
+ klass.parameter :name
268
+ writer = StringIO.new
269
+ m = klass.new({name: "John"}, stdout: writer)
270
+ expect(m.stdout).to eq(writer)
271
+ end
272
+
273
+ it "raises error when dependency is missing" do
274
+ klass = Class.new(CommandModel::Model)
275
+ klass.dependency :stdout
276
+ klass.parameter :name
277
+ expect { klass.new name: "John" }.to raise_error(StandardError)
278
+ end
279
+
280
+ it "does not raise error when allow blank dependency is missing" do
281
+ klass = Class.new(CommandModel::Model)
282
+ klass.dependency :stdout, allow_blank: true
283
+ klass.parameter :name
284
+ expect { klass.new name: "John" }.to_not raise_error
285
+ end
163
286
  end
164
287
 
165
288
  describe "call" do