command_model 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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