command_model 2.0.1 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +26 -0
- data/.gitignore +1 -1
- data/.ruby-version +1 -1
- data/.tool-versions +1 -0
- data/Gemfile.lock +50 -0
- data/README.md +133 -54
- data/Rakefile +4 -7
- data/bin/rake +27 -0
- data/command_model.gemspec +4 -5
- data/examples/bank/Gemfile +4 -4
- data/examples/bank/app/controllers/accounts_controller.rb +12 -12
- data/examples/bank/app/controllers/transfers_controller.rb +3 -3
- data/examples/bank/app/views/accounts/deposit_form.html.erb +1 -5
- data/examples/bank/app/views/accounts/withdraw_form.html.erb +1 -5
- data/examples/bank/app/views/layouts/application.html.erb +1 -1
- data/examples/bank/app/views/transfers/new.html.erb +2 -6
- data/examples/bank/config/application.rb +1 -1
- data/examples/bank/config/routes.rb +6 -6
- data/examples/bank/test/performance/browsing_test.rb +2 -2
- data/gemfiles/{5.0.gemfile → 6.1.gemfile} +1 -1
- data/gemfiles/6.1.gemfile.lock +52 -0
- data/gemfiles/{5.1.gemfile → 7.0.gemfile} +1 -1
- data/gemfiles/7.0.gemfile.lock +50 -0
- data/gemfiles/7.1.gemfile +5 -0
- data/gemfiles/7.1.gemfile.lock +60 -0
- data/lib/command_model/model.rb +61 -8
- data/lib/command_model/version.rb +1 -1
- data/spec/model_spec.rb +129 -6
- metadata +20 -13
- data/.travis.yml +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae545ab3c7c527111971b821772770a8b050f897550fa3512c6476edd67182d1
|
4
|
+
data.tar.gz: be5789d54e5e90730445343e9d29bd789c6a3f5f42e34a5f78075996ae51d929
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 02613c5fed0d0518ac3bff0a2d7b0fc77974d323693e1f18068d767c28bc4cc7521bb7fdece9a49ef5ef8c82a4213c098edb61972a391cb6b2be90570efc349c
|
7
|
+
data.tar.gz: d683d0d3823c268784c1821c9defed0f68fb3cc7c5cda98440d2676bae7845d8b2e6aa94de6f5ce3665b9235f7bd25efe1ec51b312342441efec370feb9d2ea9
|
@@ -0,0 +1,26 @@
|
|
1
|
+
name: "Ruby CI"
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
branches: ["master"]
|
5
|
+
pull_request:
|
6
|
+
branches: ["master"]
|
7
|
+
jobs:
|
8
|
+
test:
|
9
|
+
strategy:
|
10
|
+
fail-fast: false
|
11
|
+
matrix:
|
12
|
+
ruby: ['3.2', '3.3']
|
13
|
+
gemfile: [ "6.1", "7.0", "7.1" ]
|
14
|
+
runs-on: ubuntu-latest
|
15
|
+
env:
|
16
|
+
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
|
17
|
+
steps:
|
18
|
+
- name: Checkout code
|
19
|
+
uses: actions/checkout@v4
|
20
|
+
- name: Install Ruby and gems
|
21
|
+
uses: ruby/setup-ruby@v1
|
22
|
+
with:
|
23
|
+
ruby-version: ${{ matrix.ruby }}
|
24
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
25
|
+
- name: Run tests
|
26
|
+
run: bin/rake
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
3.2.2
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 3.2.2
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
command_model (2.1.0)
|
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.2)
|
18
|
+
diff-lcs (1.5.1)
|
19
|
+
i18n (1.12.0)
|
20
|
+
concurrent-ruby (~> 1.0)
|
21
|
+
minitest (5.18.0)
|
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-22
|
41
|
+
arm64-darwin-23
|
42
|
+
x86_64-linux
|
43
|
+
|
44
|
+
DEPENDENCIES
|
45
|
+
command_model!
|
46
|
+
rake (~> 13.1.0)
|
47
|
+
rspec (~> 3.13.0)
|
48
|
+
|
49
|
+
BUNDLED WITH
|
50
|
+
2.4.3
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
[![Build Status](https://
|
1
|
+
[![Build Status](https://github.com/jackc/command_model/actions/workflows/ci.yml/badge.svg)](https://github.com/jackc/command_model/actions/workflows/ci.yml)
|
2
2
|
|
3
3
|
# CommandModel
|
4
4
|
|
@@ -14,11 +14,13 @@ to work with once your domain operations become more complex. Domain models
|
|
14
14
|
usually have richer behavior than can be represented with a typical
|
15
15
|
ActiveRecord style update_attributes.
|
16
16
|
|
17
|
-
|
18
|
-
|
17
|
+
```ruby
|
18
|
+
# yuck!
|
19
|
+
account.update_attributes balance: account.balance - 50
|
19
20
|
|
20
|
-
|
21
|
-
|
21
|
+
# much better
|
22
|
+
account.withdraw amount: 50
|
23
|
+
```
|
22
24
|
|
23
25
|
But there are multiple complications with the OO approach. How do we integrate
|
24
26
|
Rails style validations? How are user-supplied strings type converted? How do we
|
@@ -28,27 +30,35 @@ know if the command succeeded? CommandModel solves these problems.
|
|
28
30
|
|
29
31
|
Add this line to your application's Gemfile:
|
30
32
|
|
31
|
-
|
33
|
+
```ruby
|
34
|
+
gem 'command_model'
|
35
|
+
```
|
32
36
|
|
33
37
|
And then execute:
|
34
38
|
|
35
|
-
|
39
|
+
```console
|
40
|
+
$ bundle
|
41
|
+
```
|
36
42
|
|
37
43
|
Or install it yourself as:
|
38
44
|
|
39
|
-
|
45
|
+
```console
|
46
|
+
$ gem install command_model
|
47
|
+
```
|
40
48
|
|
41
49
|
## Usage
|
42
50
|
|
43
51
|
Create a class derived from CommandModel::Model to represent the command
|
44
52
|
request.
|
45
53
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
54
|
+
```ruby
|
55
|
+
class WithdrawCommand < CommandModel::Model
|
56
|
+
parameter :amount,
|
57
|
+
convert: :integer,
|
58
|
+
presence: true,
|
59
|
+
numericality: { greater_than: 0, less_than_or_equal_to: 500 }
|
60
|
+
end
|
61
|
+
```
|
52
62
|
|
53
63
|
Create the method to run the command. This method should instantiate and call a new command object. It must pass call
|
54
64
|
a block that actually does the work. The block will only be called if
|
@@ -57,32 +67,36 @@ any further validations that only can be done during execution. If it adds
|
|
57
67
|
any errors to the command object then the command will be considered to have
|
58
68
|
failed. Finally, the call method will return self.
|
59
69
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
def withdraw(args)
|
64
|
-
WithdrawCommand.new(args).call do |command|
|
65
|
-
if balance >= command.amount
|
66
|
-
@balance -= command.amount
|
67
|
-
else
|
68
|
-
command.errors.add :amount, "is more than account balance"
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
70
|
+
```ruby
|
71
|
+
class Account
|
72
|
+
# ...
|
72
73
|
|
73
|
-
|
74
|
+
def withdraw(args)
|
75
|
+
WithdrawCommand.new(args).call do |command|
|
76
|
+
if balance >= command.amount
|
77
|
+
@balance -= command.amount
|
78
|
+
else
|
79
|
+
command.errors.add :amount, "is more than account balance"
|
80
|
+
end
|
74
81
|
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# ...
|
85
|
+
end
|
86
|
+
```
|
75
87
|
|
76
88
|
Use example:
|
77
89
|
|
78
|
-
|
90
|
+
```ruby
|
91
|
+
response = account.withdraw amount: 50
|
79
92
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
93
|
+
if response.success?
|
94
|
+
puts "Success!"
|
95
|
+
else
|
96
|
+
puts "Errors:"
|
97
|
+
puts response.errors.full_messages
|
98
|
+
end
|
99
|
+
```
|
86
100
|
|
87
101
|
## Mixing in Domain Logic
|
88
102
|
|
@@ -93,28 +107,73 @@ method. The execute method is called by the call method if all validations
|
|
93
107
|
succeed. The following is a reimplementation of the previous example with
|
94
108
|
internal domain logic.
|
95
109
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
110
|
+
```ruby
|
111
|
+
class WithdrawCommand < CommandModel::Model
|
112
|
+
parameter :amount,
|
113
|
+
convert: :integer,
|
114
|
+
presence: true,
|
115
|
+
numericality: { greater_than: 0, less_than_or_equal_to: 500 }
|
116
|
+
parameter :account_id, presence: true
|
117
|
+
|
118
|
+
def execute
|
119
|
+
account = Account.find_by_id account_id
|
120
|
+
unless account
|
121
|
+
errors.add :account_id, "not found"
|
122
|
+
return
|
123
|
+
end
|
124
|
+
|
125
|
+
if account.balance >= amount
|
126
|
+
account.balance -= amount
|
127
|
+
else
|
128
|
+
errors.add :amount, "is more than account balance"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
## Dependencies
|
135
|
+
|
136
|
+
Sometimes a command has requires values that are not supplied by the user. For example, a command may require the
|
137
|
+
current user, a database connection, or a logger. These dependencies can be specified with the `dependency` method. The following example adds filtering to ensure that the current user owns the account.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
class WithdrawCommand < CommandModel::Model
|
141
|
+
dependency :current_user
|
142
|
+
|
143
|
+
parameter :amount,
|
144
|
+
convert: :integer,
|
145
|
+
presence: true,
|
146
|
+
numericality: { greater_than: 0, less_than_or_equal_to: 500 }
|
147
|
+
parameter :account_id, presence: true
|
148
|
+
|
149
|
+
def execute
|
150
|
+
account = current_user.accounts.find_by_id account_id
|
151
|
+
unless account
|
152
|
+
errors.add :account_id, "not found"
|
153
|
+
return
|
116
154
|
end
|
117
155
|
|
156
|
+
if account.balance >= amount
|
157
|
+
account.balance -= amount
|
158
|
+
else
|
159
|
+
errors.add :amount, "is more than account balance"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
This command could be called as follows.
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
WithdrawCommand.execute({amount: 200}, {current_user: current_user})
|
169
|
+
```
|
170
|
+
|
171
|
+
## Inheritance
|
172
|
+
|
173
|
+
Subclasses of `CommandModel::Model` can themselves be subclassed. Parameter and dependency definitions will be copied at
|
174
|
+
the time of subclassing. If the superclass is later modified, the parameter and dependency definitions of the subclasses
|
175
|
+
will not be modified. This may be changed in the future.
|
176
|
+
|
118
177
|
## Other uses
|
119
178
|
|
120
179
|
This could be used to wrap database generated errors into normal Rails
|
@@ -128,8 +187,28 @@ be doing a uniqueness check anyway.
|
|
128
187
|
There is a simple Rails application in examples/bank that demonstrates the
|
129
188
|
integration of Rails form helpers and validations with CommandModel.
|
130
189
|
|
190
|
+
## Testing
|
191
|
+
|
192
|
+
```console
|
193
|
+
$ bundle install
|
194
|
+
$ rake
|
195
|
+
```
|
196
|
+
|
197
|
+
To test against a specific version of Rails / ActiveModel set the BUNDLE_GEMFILE environment variable. For example, to
|
198
|
+
test Rails 6.1:
|
199
|
+
|
200
|
+
```console
|
201
|
+
$ BUNDLE_GEMFILE=gemfiles/6.1.gemfile bundle install
|
202
|
+
$ BUNDLE_GEMFILE=gemfiles/6.1.gemfile rake
|
203
|
+
```
|
204
|
+
|
131
205
|
## Version History
|
132
206
|
|
207
|
+
* 2.1.0 - March 15, 2023
|
208
|
+
* Add dependencies to CommandModel::Model
|
209
|
+
* Allow inheritance of CommandModel::Model
|
210
|
+
* Require Ruby 3.2+
|
211
|
+
* Require Rails / ActiveModel 6.1+
|
133
212
|
* 2.0.1 - April 3, 2023
|
134
213
|
* Date parsing allows 5 digit years
|
135
214
|
* 2.0 - April 11, 2018
|
data/Rakefile
CHANGED
@@ -1,10 +1,7 @@
|
|
1
|
-
|
1
|
+
require "bundler/setup"
|
2
2
|
require "bundler/gem_tasks"
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
RSpec::Core::RakeTask.new(:spec)
|
7
|
-
rescue LoadError
|
8
|
-
end
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
9
6
|
|
10
|
-
task :
|
7
|
+
task default: :spec
|
data/bin/rake
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
12
|
+
|
13
|
+
bundle_binstub = File.expand_path("bundle", __dir__)
|
14
|
+
|
15
|
+
if File.file?(bundle_binstub)
|
16
|
+
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
|
17
|
+
load(bundle_binstub)
|
18
|
+
else
|
19
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
20
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require "rubygems"
|
25
|
+
require "bundler/setup"
|
26
|
+
|
27
|
+
load Gem.bin_path("rake", "rake")
|
data/command_model.gemspec
CHANGED
@@ -9,16 +9,15 @@ Gem::Specification.new do |gem|
|
|
9
9
|
gem.homepage = "https://github.com/JackC/command_model"
|
10
10
|
|
11
11
|
gem.files = `git ls-files`.split($\)
|
12
|
-
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
12
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
13
|
gem.name = "command_model"
|
15
14
|
gem.require_paths = ["lib"]
|
16
15
|
gem.version = CommandModel::VERSION
|
17
16
|
|
18
|
-
gem.required_ruby_version = '>= 2.
|
17
|
+
gem.required_ruby_version = '>= 3.2.0'
|
19
18
|
|
20
|
-
gem.add_dependency 'activemodel', ">
|
19
|
+
gem.add_dependency 'activemodel', "> 6.1"
|
21
20
|
|
22
|
-
gem.add_development_dependency 'rake', "~>
|
23
|
-
gem.add_development_dependency 'rspec', "~> 3.
|
21
|
+
gem.add_development_dependency 'rake', "~> 13.1.0"
|
22
|
+
gem.add_development_dependency 'rspec', "~> 3.13.0"
|
24
23
|
end
|
data/examples/bank/Gemfile
CHANGED
@@ -3,9 +3,9 @@ source 'https://rubygems.org'
|
|
3
3
|
gem 'rails', '3.2.8'
|
4
4
|
|
5
5
|
# Bundle edge Rails instead:
|
6
|
-
# gem 'rails', :
|
6
|
+
# gem 'rails', git: 'git://github.com/rails/rails.git'
|
7
7
|
|
8
|
-
gem 'command_model', :
|
8
|
+
gem 'command_model', path: '../..'
|
9
9
|
|
10
10
|
|
11
11
|
# Gems used only for assets and not required
|
@@ -15,7 +15,7 @@ group :assets do
|
|
15
15
|
gem 'coffee-rails', '~> 3.2.1'
|
16
16
|
|
17
17
|
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
|
18
|
-
# gem 'therubyracer', :
|
18
|
+
# gem 'therubyracer', platform: :ruby
|
19
19
|
|
20
20
|
gem 'uglifier', '>= 1.0.3'
|
21
21
|
end
|
@@ -35,4 +35,4 @@ gem 'jquery-rails'
|
|
35
35
|
# gem 'capistrano'
|
36
36
|
|
37
37
|
# To use debugger
|
38
|
-
# gem 'ruby-debug19', :
|
38
|
+
# gem 'ruby-debug19', require: 'ruby-debug'
|
@@ -7,44 +7,44 @@
|
|
7
7
|
@account = Account.find_by_name params[:id]
|
8
8
|
@deposit = Account::DepositCommand.new
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
def deposit
|
12
12
|
@account = Account.find_by_name params[:id]
|
13
13
|
@deposit = @account.deposit params[:deposit]
|
14
|
-
|
14
|
+
|
15
15
|
if @deposit.success?
|
16
|
-
redirect_to root_path, :
|
16
|
+
redirect_to root_path, notice: "Deposited #{@deposit.amount} to #{@account.name}'s account."
|
17
17
|
else
|
18
18
|
render "deposit_form"
|
19
19
|
end
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def withdraw_form
|
23
23
|
@account = Account.find_by_name params[:id]
|
24
24
|
@withdraw = Account::WithdrawCommand.new
|
25
|
-
end
|
26
|
-
|
25
|
+
end
|
26
|
+
|
27
27
|
def withdraw
|
28
28
|
@account = Account.find_by_name params[:id]
|
29
29
|
@withdraw = @account.withdraw params[:withdraw]
|
30
|
-
|
30
|
+
|
31
31
|
if @withdraw.success?
|
32
|
-
redirect_to root_path, :
|
32
|
+
redirect_to root_path, notice: "Withdrew #{@withdraw.amount} from #{@account.name}'s account."
|
33
33
|
else
|
34
34
|
render "withdraw_form"
|
35
|
-
end
|
35
|
+
end
|
36
36
|
end
|
37
37
|
|
38
38
|
def transfer_form
|
39
39
|
@accounts = Account.all
|
40
40
|
@transfer = Account::TransferCommand.new
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
def transfer
|
44
44
|
@transfer = Account::TransferCommand.new(params[:transfer])
|
45
|
-
|
45
|
+
|
46
46
|
if @transfer.call.success?
|
47
|
-
redirect_to root_path, :
|
47
|
+
redirect_to root_path, notice: "Transferred #{@transfer.amount} from #{@transfer.from.name}'s account to #{@transfer.to.name}'s account."
|
48
48
|
else
|
49
49
|
@accounts = Account.all
|
50
50
|
render "transfer_form"
|
@@ -3,12 +3,12 @@ class TransfersController < ApplicationController
|
|
3
3
|
@accounts = Account.all
|
4
4
|
@transfer = Account::Transfer.new
|
5
5
|
end
|
6
|
-
|
6
|
+
|
7
7
|
def create
|
8
8
|
@transfer = Account::Transfer.new(params[:transfer])
|
9
|
-
|
9
|
+
|
10
10
|
if @transfer.call.success?
|
11
|
-
redirect_to root_path, :
|
11
|
+
redirect_to root_path, notice: "Transferred #{@transfer.amount} from #{@transfer.from.name}'s account to #{@transfer.to.name}'s account."
|
12
12
|
else
|
13
13
|
@accounts = Account.all
|
14
14
|
render :new
|
@@ -12,12 +12,8 @@
|
|
12
12
|
</ul>
|
13
13
|
<% end %>
|
14
14
|
|
15
|
-
<%= form_for @deposit, :
|
15
|
+
<%= form_for @deposit, as: :deposit, url: account_deposit_path(@account) do |f| %>
|
16
16
|
<%= f.label :amount %>
|
17
17
|
<%= f.text_field :amount %>
|
18
18
|
<%= f.submit "Deposit" %>
|
19
19
|
<% end %>
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
@@ -12,12 +12,8 @@
|
|
12
12
|
</ul>
|
13
13
|
<% end %>
|
14
14
|
|
15
|
-
<%= form_for @withdraw, :
|
15
|
+
<%= form_for @withdraw, as: :withdraw, url: account_withdraw_path(@account) do |f| %>
|
16
16
|
<%= f.label :amount %>
|
17
17
|
<%= f.text_field :amount %>
|
18
18
|
<%= f.submit "Deposit" %>
|
19
19
|
<% end %>
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
@@ -14,11 +14,11 @@
|
|
14
14
|
<%= form_for @transfer do |f| %>
|
15
15
|
<div>
|
16
16
|
<%= f.label :from_name %><br />
|
17
|
-
<%= f.select :from_name, @accounts.map { |a| ["#{a.name} - #{a.balance}", a.name] }, :
|
17
|
+
<%= f.select :from_name, @accounts.map { |a| ["#{a.name} - #{a.balance}", a.name] }, selected: f.object.from_name, include_blank: true %>
|
18
18
|
</div>
|
19
19
|
<div>
|
20
20
|
<%= f.label :to_name %><br />
|
21
|
-
<%= f.select :to_name, @accounts.map { |a| ["#{a.name} - #{a.balance}", a.name] }, :
|
21
|
+
<%= f.select :to_name, @accounts.map { |a| ["#{a.name} - #{a.balance}", a.name] }, selected: f.object.to_name, include_blank: true %>
|
22
22
|
</div>
|
23
23
|
<div>
|
24
24
|
<%= f.label :amount %><br />
|
@@ -26,7 +26,3 @@
|
|
26
26
|
</div>
|
27
27
|
<%= f.submit "Transfer" %>
|
28
28
|
<% end %>
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
@@ -10,7 +10,7 @@ require "rails/test_unit/railtie"
|
|
10
10
|
|
11
11
|
if defined?(Bundler)
|
12
12
|
# If you precompile assets before deploying to production, use this line
|
13
|
-
Bundler.require(*Rails.groups(:
|
13
|
+
Bundler.require(*Rails.groups(assets: %w(development test)))
|
14
14
|
# If you want your assets lazily compiled in production, use this line
|
15
15
|
# Bundler.require(:default, :assets, Rails.env)
|
16
16
|
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
Bank::Application.routes.draw do
|
2
|
-
get "accounts/:id/withdraw" => "accounts#withdraw_form", :
|
3
|
-
post "accounts/:id/withdraw" => "accounts#withdraw", :
|
4
|
-
|
5
|
-
get "accounts/:id/deposit" => "accounts#deposit_form", :
|
6
|
-
post "accounts/:id/deposit" => "accounts#deposit", :
|
2
|
+
get "accounts/:id/withdraw" => "accounts#withdraw_form", as: :account_withdraw_form
|
3
|
+
post "accounts/:id/withdraw" => "accounts#withdraw", as: :account_withdraw
|
4
|
+
|
5
|
+
get "accounts/:id/deposit" => "accounts#deposit_form", as: :account_deposit_form
|
6
|
+
post "accounts/:id/deposit" => "accounts#deposit", as: :account_deposit
|
7
7
|
|
8
8
|
resources :transfers, only: %w[new create]
|
9
9
|
|
10
|
-
root :
|
10
|
+
root to: "accounts#index"
|
11
11
|
end
|
@@ -3,8 +3,8 @@ require 'rails/performance_test_help'
|
|
3
3
|
|
4
4
|
class BrowsingTest < ActionDispatch::PerformanceTest
|
5
5
|
# Refer to the documentation for all available options
|
6
|
-
# self.profile_options = { :
|
7
|
-
# :
|
6
|
+
# self.profile_options = { runs: 5, metrics: [:wall_time, :memory]
|
7
|
+
# output: 'tmp/performance', formats: [:flat] }
|
8
8
|
|
9
9
|
def test_homepage
|
10
10
|
get '/'
|
@@ -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
|
@@ -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,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
|
data/lib/command_model/model.rb
CHANGED
@@ -4,7 +4,12 @@ module CommandModel
|
|
4
4
|
include ActiveModel::Conversion
|
5
5
|
extend ActiveModel::Naming
|
6
6
|
|
7
|
-
|
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)
|
8
13
|
|
9
14
|
# Parameter requires one or more attributes as its first parameter(s).
|
10
15
|
# It accepts an options hash as its last parameter.
|
@@ -26,11 +31,12 @@ module CommandModel
|
|
26
31
|
# parameter :height, :weight,
|
27
32
|
# convert: [CommandModel::Convert::StringMutator.new { |s| s.gsub(",", "")}, :integer],
|
28
33
|
# presence: true,
|
29
|
-
# numericality: { :
|
34
|
+
# numericality: { greater_than_or_equal_to: 0 }
|
30
35
|
def self.parameter(*args)
|
31
36
|
options = args.last.kind_of?(Hash) ? args.pop.clone : {}
|
32
37
|
converters = options.delete(:convert)
|
33
38
|
|
39
|
+
@parameters ||= [].freeze
|
34
40
|
args.each do |name|
|
35
41
|
attr_reader name
|
36
42
|
|
@@ -40,13 +46,13 @@ module CommandModel
|
|
40
46
|
attr_writer name
|
41
47
|
end
|
42
48
|
validates name, options.clone if options.present? # clone options because validates mutates the hash :(
|
43
|
-
parameters
|
49
|
+
@parameters = (@parameters + [Parameter.new(name, converters, options)]).freeze
|
44
50
|
end
|
45
51
|
end
|
46
52
|
|
47
53
|
# Returns array of all parameters defined for class
|
48
54
|
def self.parameters
|
49
|
-
@parameters ||= []
|
55
|
+
@parameters ||= [].freeze
|
50
56
|
end
|
51
57
|
|
52
58
|
def self.attr_type_converting_writer(name, converters) #:nodoc
|
@@ -82,6 +88,38 @@ module CommandModel
|
|
82
88
|
end
|
83
89
|
end
|
84
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
|
+
|
85
123
|
# Executes a block of code if the command model is valid.
|
86
124
|
#
|
87
125
|
# Accepts either a command model or a hash of attributes with which to
|
@@ -89,18 +127,19 @@ module CommandModel
|
|
89
127
|
#
|
90
128
|
# ==== Examples
|
91
129
|
#
|
92
|
-
# RenameUserCommand.execute(:
|
130
|
+
# RenameUserCommand.execute(login: "john") do |command|
|
93
131
|
# if allowed_to_rename_user?
|
94
132
|
# self.login = command.login
|
95
133
|
# else
|
96
134
|
# command.errors.add :base, "not allowed to rename"
|
97
135
|
# end
|
98
136
|
# end
|
99
|
-
def self.execute(attributes_or_command, &block)
|
137
|
+
def self.execute(attributes_or_command, dependencies={}, &block)
|
100
138
|
command = if attributes_or_command.kind_of? self
|
139
|
+
raise ArgumentError, "cannot pass dependencies with already initialized command" if dependencies.present?
|
101
140
|
attributes_or_command
|
102
141
|
else
|
103
|
-
new(attributes_or_command)
|
142
|
+
new(attributes_or_command, dependencies)
|
104
143
|
end
|
105
144
|
|
106
145
|
command.call &block
|
@@ -129,9 +168,23 @@ module CommandModel
|
|
129
168
|
# Accepts a parameters hash or another of the same class. If another
|
130
169
|
# instance of the same class is passed in then the parameters are copied
|
131
170
|
# to the new object.
|
132
|
-
def initialize(parameters={})
|
171
|
+
def initialize(parameters={}, dependencies={})
|
133
172
|
@type_conversion_errors = {}
|
134
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
|
135
188
|
end
|
136
189
|
|
137
190
|
# Executes the command by calling the method +execute+ if the validations
|
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 :
|
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, :
|
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, :
|
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(:
|
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 :
|
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 :
|
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
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: command_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jack Christensen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -16,42 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '6.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '6.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 13.1.0
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 13.1.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 3.
|
47
|
+
version: 3.13.0
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 3.
|
54
|
+
version: 3.13.0
|
55
55
|
description: CommandModel - when update_attributes isn't enough.
|
56
56
|
email:
|
57
57
|
- jack@jackchristensen.com
|
@@ -59,14 +59,17 @@ executables: []
|
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
61
61
|
files:
|
62
|
+
- ".github/workflows/ci.yml"
|
62
63
|
- ".gitignore"
|
63
64
|
- ".rspec"
|
64
65
|
- ".ruby-version"
|
65
|
-
- ".
|
66
|
+
- ".tool-versions"
|
66
67
|
- Gemfile
|
68
|
+
- Gemfile.lock
|
67
69
|
- LICENSE
|
68
70
|
- README.md
|
69
71
|
- Rakefile
|
72
|
+
- bin/rake
|
70
73
|
- command_model.gemspec
|
71
74
|
- examples/bank/.gitignore
|
72
75
|
- examples/bank/Gemfile
|
@@ -128,8 +131,12 @@ files:
|
|
128
131
|
- examples/bank/vendor/assets/javascripts/.gitkeep
|
129
132
|
- examples/bank/vendor/assets/stylesheets/.gitkeep
|
130
133
|
- examples/bank/vendor/plugins/.gitkeep
|
131
|
-
- gemfiles/
|
132
|
-
- gemfiles/
|
134
|
+
- gemfiles/6.1.gemfile
|
135
|
+
- gemfiles/6.1.gemfile.lock
|
136
|
+
- gemfiles/7.0.gemfile
|
137
|
+
- gemfiles/7.0.gemfile.lock
|
138
|
+
- gemfiles/7.1.gemfile
|
139
|
+
- gemfiles/7.1.gemfile.lock
|
133
140
|
- lib/command_model.rb
|
134
141
|
- lib/command_model/convert.rb
|
135
142
|
- lib/command_model/model.rb
|
@@ -148,14 +155,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
148
155
|
requirements:
|
149
156
|
- - ">="
|
150
157
|
- !ruby/object:Gem::Version
|
151
|
-
version: 2.
|
158
|
+
version: 3.2.0
|
152
159
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
153
160
|
requirements:
|
154
161
|
- - ">="
|
155
162
|
- !ruby/object:Gem::Version
|
156
163
|
version: '0'
|
157
164
|
requirements: []
|
158
|
-
rubygems_version: 3.4.
|
165
|
+
rubygems_version: 3.4.10
|
159
166
|
signing_key:
|
160
167
|
specification_version: 4
|
161
168
|
summary: CommandModel integrates Rails validations with command objects. This allows
|