subroutine 1.0.0.rc4 → 2.0.0.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.MD +111 -0
- data/Gemfile +1 -1
- data/README.md +22 -9
- data/gemfiles/am41.gemfile +3 -1
- data/gemfiles/am42.gemfile +2 -0
- data/gemfiles/am50.gemfile +2 -1
- data/gemfiles/am51.gemfile +2 -1
- data/gemfiles/am52.gemfile +2 -1
- data/gemfiles/am60.gemfile +2 -1
- data/lib/subroutine/association_fields/configuration.rb +24 -9
- data/lib/subroutine/association_fields.rb +18 -15
- data/lib/subroutine/auth.rb +12 -5
- data/lib/subroutine/fields/configuration.rb +8 -2
- data/lib/subroutine/fields.rb +20 -2
- data/lib/subroutine/op.rb +16 -27
- data/lib/subroutine/type_caster.rb +9 -0
- data/lib/subroutine/version.rb +2 -2
- data/subroutine.gemspec +10 -7
- data/test/subroutine/association_test.rb +78 -7
- data/test/subroutine/auth_test.rb +12 -0
- data/test/subroutine/base_test.rb +38 -0
- data/test/subroutine/type_caster_test.rb +23 -0
- data/test/support/ops.rb +55 -1
- data/test/test_helper.rb +7 -6
- metadata +34 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc8eb530552fea8e2bacd033372826e11c57734d9695973ebca956c76aa2ed93
|
4
|
+
data.tar.gz: 908764a3fc127881b4c719c2c5523ae280771683e765ea8f47ddda52bed76a33
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4de2ad64fede40a510afe5b0af073eb6fae1800a61e2f9387337c1d46475ec439c1c35cb393ecececb3a5954b7c9e1b32be10e6be963dc7ce3fb840d40a3b368
|
7
|
+
data.tar.gz: 9ec24f1a6ce47edb502bded197096dec4d1ff444c4d3b0fda64291111aa7a29e67e7bd4f8e01a018c72795117c40e16fa3c68131b64db346e3c1073611c8134e
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.5.
|
1
|
+
2.5.7
|
data/CHANGELOG.MD
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
## Subroutine 1.0
|
2
|
+
|
3
|
+
A massive refactor took place between 0.9 and 1.0, including breaking changes. The goal was to reduce complexity, simplify backtraces, and increase the overall safety and reliability of the library.
|
4
|
+
|
5
|
+
### Subroutine::Fields
|
6
|
+
|
7
|
+
`Subroutine::Fields` was completely refactored to manage field declaration, configuration, and access in a more systematic and safer way.
|
8
|
+
|
9
|
+
`Op._fields` was removed in favor of `Op.field_configurations`. `field_configurations` is a hash with keys of the field name and values of `FieldConfiguration` objects. FieldConfiguration objects are SimpleDelegates to the underlying option hashes. They answer questions and provide a mechanism for validating the configuration of a field.
|
10
|
+
|
11
|
+
Fields can be accessed via helpers and accessors can be managed by the field declration. Helpers include `get_field`, `set_field`, and `clear_field`.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class SomeOp < ::Subroutine::Op
|
15
|
+
string :foo, read_accessor: field_reader: false, field_writer: true
|
16
|
+
|
17
|
+
def perform
|
18
|
+
self.foo = "bar"
|
19
|
+
self.foo # NoMethodError
|
20
|
+
self.get_field(:foo) # => "bar"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
Fields can be omitted from mass assignment, meaning they would not be respected via constructor signatures.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class SomeOp < ::Subroutine::Op
|
29
|
+
string :foo, mass_assignable: false
|
30
|
+
def perform
|
31
|
+
puts foo
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
SomeOp.submit!(foo: "Hello World!") # raises ::Subroutine::Fields::MassAssignmentError
|
36
|
+
SomeOp.new{|op| op.foo = "Hello World!" }.submit! # prints "Hello World!"
|
37
|
+
```
|
38
|
+
|
39
|
+
This is especially useful when dealing with user input and potentially unsafe attributes.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class UserUpdateOp < ::Op
|
43
|
+
association :user
|
44
|
+
string :first_name
|
45
|
+
string :last_name
|
46
|
+
integer :credit_balance_cents, mass_assignable: false
|
47
|
+
|
48
|
+
def perform
|
49
|
+
user.update(params)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# some_controller.rb
|
54
|
+
def update
|
55
|
+
UserUpdateOp.submit!(params.merge(user: current_user))
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
Field groups were added as well, allowing you to access subsets of the fields easily.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
class AccountUpdateOp < ::Op
|
63
|
+
association :account
|
64
|
+
|
65
|
+
with_options group: :user do
|
66
|
+
string :first_name
|
67
|
+
string :last_name
|
68
|
+
date :dob
|
69
|
+
end
|
70
|
+
|
71
|
+
with_options group: :business do
|
72
|
+
string :company_name
|
73
|
+
string :ein
|
74
|
+
end
|
75
|
+
|
76
|
+
def perform
|
77
|
+
account.user.update(user_params)
|
78
|
+
account.business.update(business_params)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
ActionController::Parameters from Rails 5+ are now transformed to a hash in `Subroutine::Fields` by default. This means strong parameters are essentially unused when passing `Subroutine::Fields`.
|
85
|
+
|
86
|
+
Read more about field management and access in https://github.com/guideline-tech/subroutine/wiki/Param-Usage
|
87
|
+
|
88
|
+
### Subroutine::Association
|
89
|
+
|
90
|
+
The `Subroutine::Association` module has been moved to `Subroutine::AssociationFields`.
|
91
|
+
|
92
|
+
Only native types are stored in params now. The objects loaded from associations are stored in an `association_cache`. This ensures access to fields are consistent regardless of the inputs.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class SomeOp < ::Subroutine::Op
|
96
|
+
association :user
|
97
|
+
association :resource, polymorphic: true
|
98
|
+
end
|
99
|
+
|
100
|
+
user = User.find(4)
|
101
|
+
|
102
|
+
op = SomeOp.new(user: user, resource: user)
|
103
|
+
op.params #=> { user_id: 4, resource_type: "User", resource_id: 4 }
|
104
|
+
op.params_with_association #=> { user: <User:103204 @id=4>, resource: <User:103204 @id=4> }
|
105
|
+
|
106
|
+
op = SomeOp.new(user_id: user.id, resource_type: "User", resource_id: user.id)
|
107
|
+
op.params #=> { user_id: 4, resource_type: "User", resource_id: 4 }
|
108
|
+
op.params_with_association #=> { user: <User:290053 @id=4>, resource: <User:29042 @id=4> }
|
109
|
+
```
|
110
|
+
|
111
|
+
Assignment of associations now validates the type. If an association is not polymorphic, the type will be validated against the expected type.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -13,26 +13,37 @@ class SignupOp < ::Subroutine::Op
|
|
13
13
|
string :email
|
14
14
|
string :password
|
15
15
|
|
16
|
+
string :company_name
|
17
|
+
|
16
18
|
validates :name, presence: true
|
17
19
|
validates :email, presence: true
|
18
20
|
validates :password, presence: true
|
21
|
+
validates :company_name, presence: true
|
19
22
|
|
20
|
-
outputs :
|
23
|
+
outputs :user
|
24
|
+
outputs :business
|
21
25
|
|
22
26
|
protected
|
23
27
|
|
24
28
|
def perform
|
25
29
|
u = create_user!
|
26
|
-
|
30
|
+
b = create_business!(u)
|
31
|
+
|
32
|
+
deliver_welcome_email(u)
|
27
33
|
|
28
|
-
output :
|
34
|
+
output :user, u
|
35
|
+
output :business, b
|
29
36
|
end
|
30
37
|
|
31
38
|
def create_user!
|
32
|
-
User.create!(
|
39
|
+
User.create!(name: name, email: email, password: password)
|
33
40
|
end
|
34
41
|
|
35
|
-
def
|
42
|
+
def create_business!(owner)
|
43
|
+
Business.create!(company_name: company_name, owner: owner)
|
44
|
+
end
|
45
|
+
|
46
|
+
def deliver_welcome_email(u)
|
36
47
|
UserMailer.welcome(u.id).deliver_later
|
37
48
|
end
|
38
49
|
end
|
@@ -45,7 +56,9 @@ end
|
|
45
56
|
- Clear and concise intention in a single file
|
46
57
|
- Multi-model operations become simple
|
47
58
|
|
48
|
-
|
49
|
-
|
50
|
-
[
|
51
|
-
[
|
59
|
+
## Continue Reading
|
60
|
+
|
61
|
+
- [Implementing an Op](https://github.com/guideline-tech/subroutine/wiki/Implementing-an-Op)
|
62
|
+
- [Using an Op](https://github.com/guideline-tech/subroutine/wiki/Using-an-Op)
|
63
|
+
- [Errors](https://github.com/guideline-tech/subroutine/wiki/Errors)
|
64
|
+
- [Basic Usage in Rails](https://github.com/guideline-tech/subroutine/wiki/Rails-Usage)
|
data/gemfiles/am41.gemfile
CHANGED
data/gemfiles/am42.gemfile
CHANGED
data/gemfiles/am50.gemfile
CHANGED
data/gemfiles/am51.gemfile
CHANGED
data/gemfiles/am52.gemfile
CHANGED
data/gemfiles/am60.gemfile
CHANGED
@@ -10,7 +10,7 @@ module Subroutine
|
|
10
10
|
def validate!
|
11
11
|
super
|
12
12
|
|
13
|
-
if as && foreign_key
|
13
|
+
if config[:as] && foreign_key
|
14
14
|
raise ArgumentError, ":as and :foreign_key options should not be provided together to an association invocation"
|
15
15
|
end
|
16
16
|
end
|
@@ -19,6 +19,13 @@ module Subroutine
|
|
19
19
|
super + [::Subroutine::AssociationFields]
|
20
20
|
end
|
21
21
|
|
22
|
+
def related_field_names
|
23
|
+
out = super
|
24
|
+
out << foreign_key_method
|
25
|
+
out << foreign_type_method if polymorphic?
|
26
|
+
out
|
27
|
+
end
|
28
|
+
|
22
29
|
def polymorphic?
|
23
30
|
!!config[:polymorphic]
|
24
31
|
end
|
@@ -27,12 +34,13 @@ module Subroutine
|
|
27
34
|
config[:as] || field_name
|
28
35
|
end
|
29
36
|
|
30
|
-
def
|
31
|
-
config[:class_name]&.to_s
|
37
|
+
def foreign_type
|
38
|
+
(config[:foreign_type] || config[:class_name])&.to_s
|
32
39
|
end
|
40
|
+
alias class_name foreign_type
|
33
41
|
|
34
|
-
def
|
35
|
-
|
42
|
+
def inferred_foreign_type
|
43
|
+
foreign_type || as.to_s.camelize
|
36
44
|
end
|
37
45
|
|
38
46
|
def foreign_key
|
@@ -40,15 +48,19 @@ module Subroutine
|
|
40
48
|
end
|
41
49
|
|
42
50
|
def foreign_key_method
|
43
|
-
foreign_key || "#{field_name}_id"
|
51
|
+
(foreign_key || "#{field_name}_id").to_sym
|
44
52
|
end
|
45
53
|
|
46
54
|
def foreign_type_method
|
47
|
-
foreign_key_method.gsub(/_id$/, "_type")
|
55
|
+
foreign_key_method.to_s.gsub(/_id$/, "_type").to_sym
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_by
|
59
|
+
(config[:find_by] || :id).to_sym
|
48
60
|
end
|
49
61
|
|
50
62
|
def build_foreign_key_field
|
51
|
-
build_child_field(foreign_key_method, type: :
|
63
|
+
build_child_field(foreign_key_method, type: :foreign_key, foreign_key_type: config[:foreign_key_type])
|
52
64
|
end
|
53
65
|
|
54
66
|
def build_foreign_type_field
|
@@ -66,7 +78,10 @@ module Subroutine
|
|
66
78
|
protected
|
67
79
|
|
68
80
|
def build_child_field(name, opts = {})
|
69
|
-
|
81
|
+
child_opts = inheritable_options
|
82
|
+
child_opts.merge!(opts)
|
83
|
+
child_opts[:association_name] = as
|
84
|
+
ComponentConfiguration.new(name, child_opts)
|
70
85
|
end
|
71
86
|
|
72
87
|
end
|
@@ -74,7 +74,7 @@ module Subroutine
|
|
74
74
|
class_eval <<-EV, __FILE__, __LINE__ + 1
|
75
75
|
try(:silence_redefinition_of_method, :#{config.foreign_type_method})
|
76
76
|
def #{config.foreign_type_method}
|
77
|
-
#{config.
|
77
|
+
#{config.inferred_foreign_type.inspect}
|
78
78
|
end
|
79
79
|
EV
|
80
80
|
end
|
@@ -100,8 +100,7 @@ module Subroutine
|
|
100
100
|
|
101
101
|
excepts = []
|
102
102
|
association_fields.each_pair do |_name, config|
|
103
|
-
excepts
|
104
|
-
excepts << config.foreign_type_method if config.polymorphic?
|
103
|
+
excepts |= config.related_field_names
|
105
104
|
end
|
106
105
|
|
107
106
|
out = params.except(*excepts)
|
@@ -120,7 +119,7 @@ module Subroutine
|
|
120
119
|
if config&.behavior == :association
|
121
120
|
maybe_raise_on_association_type_mismatch!(config, value)
|
122
121
|
set_field(config.foreign_type_method, value&.class&.name, opts) if config.polymorphic?
|
123
|
-
set_field(config.foreign_key_method, value&.
|
122
|
+
set_field(config.foreign_key_method, value&.send(config.find_by), opts)
|
124
123
|
association_cache[config.field_name] = value
|
125
124
|
else
|
126
125
|
if config&.behavior == :association_component
|
@@ -138,10 +137,7 @@ module Subroutine
|
|
138
137
|
stored_result = association_cache[config.field_name]
|
139
138
|
return stored_result unless stored_result.nil?
|
140
139
|
|
141
|
-
|
142
|
-
type = config.field_reader? || !config.polymorphic? ? send(config.foreign_type_method) : get_field(config.foreign_type_method)
|
143
|
-
|
144
|
-
result = fetch_association_instance(type, fk, config.unscoped?)
|
140
|
+
result = fetch_association_instance(config)
|
145
141
|
association_cache[config.field_name] = result
|
146
142
|
else
|
147
143
|
get_field_without_association(field_name)
|
@@ -176,25 +172,32 @@ module Subroutine
|
|
176
172
|
end
|
177
173
|
end
|
178
174
|
|
179
|
-
def fetch_association_instance(
|
180
|
-
|
175
|
+
def fetch_association_instance(config)
|
176
|
+
klass =
|
177
|
+
if config.field_reader?
|
178
|
+
config.polymorphic? ? send(config.foreign_type_method) : config.inferred_foreign_type
|
179
|
+
else
|
180
|
+
get_field(config.foreign_type_method)
|
181
|
+
end
|
181
182
|
|
182
|
-
klass = _type
|
183
183
|
klass = klass.classify.constantize if klass.is_a?(String)
|
184
|
-
|
185
184
|
return nil unless klass
|
186
185
|
|
186
|
+
foreign_key = config.foreign_key_method
|
187
|
+
value = send(foreign_key)
|
188
|
+
return nil unless value
|
189
|
+
|
187
190
|
scope = klass.all
|
188
|
-
scope = scope.unscoped if
|
191
|
+
scope = scope.unscoped if config.unscoped?
|
189
192
|
|
190
|
-
scope.
|
193
|
+
scope.find_by!(config.find_by => value)
|
191
194
|
end
|
192
195
|
|
193
196
|
def maybe_raise_on_association_type_mismatch!(config, record)
|
194
197
|
return if config.polymorphic?
|
195
198
|
return if record.nil?
|
196
199
|
|
197
|
-
klass = config.
|
200
|
+
klass = config.inferred_foreign_type.constantize
|
198
201
|
|
199
202
|
return if record.class <= klass || record.class >= klass
|
200
203
|
|
data/lib/subroutine/auth.rb
CHANGED
@@ -85,17 +85,24 @@ module Subroutine
|
|
85
85
|
def initialize(*args, &block)
|
86
86
|
raise Subroutine::Auth::AuthorizationNotDeclaredError unless self.class.authorization_declared
|
87
87
|
|
88
|
-
super(args.extract_options!, &block)
|
89
|
-
|
90
88
|
@skip_auth_checks = false
|
91
89
|
|
90
|
+
inputs = case args.last
|
91
|
+
when *::Subroutine::Fields.allowed_input_classes
|
92
|
+
args.pop
|
93
|
+
else
|
94
|
+
{}
|
95
|
+
end
|
96
|
+
|
97
|
+
super(inputs, &block)
|
98
|
+
|
92
99
|
user = args.shift
|
93
100
|
|
94
|
-
|
95
|
-
@current_user = user
|
96
|
-
else
|
101
|
+
unless self.class.supported_user_class_names.include?(user.class.name)
|
97
102
|
raise ArgumentError, "current_user must be one of the following types {#{self.class.supported_user_class_names.join(",")}} but was #{user.class.name}"
|
98
103
|
end
|
104
|
+
|
105
|
+
@current_user = user
|
99
106
|
end
|
100
107
|
|
101
108
|
def skip_auth_checks!
|
@@ -7,7 +7,7 @@ module Subroutine
|
|
7
7
|
class Configuration < ::SimpleDelegator
|
8
8
|
|
9
9
|
PROTECTED_GROUP_IDENTIFIERS = %i[all original default].freeze
|
10
|
-
INHERITABLE_OPTIONS = %i[mass_assignable field_reader field_writer groups].freeze
|
10
|
+
INHERITABLE_OPTIONS = %i[mass_assignable field_reader field_writer groups aka].freeze
|
11
11
|
NO_GROUPS = [].freeze
|
12
12
|
|
13
13
|
def self.from(field_name, options)
|
@@ -22,7 +22,7 @@ module Subroutine
|
|
22
22
|
attr_reader :field_name
|
23
23
|
|
24
24
|
def initialize(field_name, options)
|
25
|
-
@field_name = field_name
|
25
|
+
@field_name = field_name.to_sym
|
26
26
|
config = sanitize_options(options)
|
27
27
|
super(config)
|
28
28
|
validate!
|
@@ -38,6 +38,10 @@ module Subroutine
|
|
38
38
|
[]
|
39
39
|
end
|
40
40
|
|
41
|
+
def related_field_names
|
42
|
+
[field_name]
|
43
|
+
end
|
44
|
+
|
41
45
|
def behavior
|
42
46
|
nil
|
43
47
|
end
|
@@ -96,6 +100,8 @@ module Subroutine
|
|
96
100
|
groups = nil if groups == false
|
97
101
|
opts[:groups] = Array(groups).map(&:to_sym).presence
|
98
102
|
opts.delete(:group)
|
103
|
+
opts[:aka] = opts[:aka].to_sym if opts[:aka]
|
104
|
+
opts[:name] = field_name
|
99
105
|
opts
|
100
106
|
end
|
101
107
|
|
data/lib/subroutine/fields.rb
CHANGED
@@ -14,6 +14,18 @@ module Subroutine
|
|
14
14
|
|
15
15
|
extend ActiveSupport::Concern
|
16
16
|
|
17
|
+
def self.allowed_input_classes
|
18
|
+
@allowed_input_classes ||= begin
|
19
|
+
out = [Hash]
|
20
|
+
out << ActionController::Parameters if action_controller_params_loaded?
|
21
|
+
out
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.action_controller_params_loaded?
|
26
|
+
defined?(::ActionController::Parameters)
|
27
|
+
end
|
28
|
+
|
17
29
|
included do
|
18
30
|
class_attribute :field_configurations
|
19
31
|
self.field_configurations = {}
|
@@ -46,9 +58,12 @@ module Subroutine
|
|
46
58
|
onlys = options.key?(:only) ? Array(options.delete(:only)) : nil
|
47
59
|
|
48
60
|
things.each do |thing|
|
61
|
+
local_excepts = excepts.map { |field| thing.get_field_config(field)&.related_field_names }.flatten.compact.uniq if excepts
|
62
|
+
local_onlys = onlys.map { |field| thing.get_field_config(field)&.related_field_names }.flatten.compact.uniq if onlys
|
63
|
+
|
49
64
|
thing.field_configurations.each_pair do |field_name, config|
|
50
|
-
next if
|
51
|
-
next if
|
65
|
+
next if local_excepts&.include?(field_name)
|
66
|
+
next if local_onlys && !local_onlys.include?(field_name)
|
52
67
|
|
53
68
|
config.required_modules.each do |mod|
|
54
69
|
include mod unless included_modules.include?(mod)
|
@@ -141,6 +156,9 @@ module Subroutine
|
|
141
156
|
end
|
142
157
|
|
143
158
|
def setup_fields(inputs = {})
|
159
|
+
if ::Subroutine::Fields.action_controller_params_loaded? && inputs.is_a?(::ActionController::Parameters)
|
160
|
+
inputs = inputs.to_unsafe_h if inputs.respond_to?(:to_unsafe_h)
|
161
|
+
end
|
144
162
|
@provided_fields = {}.with_indifferent_access
|
145
163
|
param_groups[:original] = inputs.with_indifferent_access
|
146
164
|
mass_assign_initial_params
|
data/lib/subroutine/op.rb
CHANGED
@@ -37,28 +37,11 @@ module Subroutine
|
|
37
37
|
op
|
38
38
|
end
|
39
39
|
|
40
|
-
protected
|
41
|
-
|
42
|
-
def field(field_name, options = {})
|
43
|
-
result = super(field_name, options)
|
44
|
-
|
45
|
-
if options[:aka]
|
46
|
-
Array(options[:aka]).each do |as|
|
47
|
-
self._error_map = _error_map.merge(as.to_sym => field_name.to_sym)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
result
|
52
|
-
end
|
53
|
-
|
54
40
|
end
|
55
41
|
|
56
42
|
class_attribute :_failure_class
|
57
43
|
self._failure_class = Subroutine::Failure
|
58
44
|
|
59
|
-
class_attribute :_error_map
|
60
|
-
self._error_map = {}
|
61
|
-
|
62
45
|
def initialize(inputs = {})
|
63
46
|
setup_fields(inputs)
|
64
47
|
setup_outputs
|
@@ -82,7 +65,7 @@ module Subroutine
|
|
82
65
|
|
83
66
|
if errors.empty?
|
84
67
|
validate_outputs!
|
85
|
-
|
68
|
+
self
|
86
69
|
else
|
87
70
|
raise _failure_class, self
|
88
71
|
end
|
@@ -91,6 +74,7 @@ module Subroutine
|
|
91
74
|
# the action which should be invoked upon form submission (from the controller)
|
92
75
|
def submit
|
93
76
|
submit!
|
77
|
+
true
|
94
78
|
rescue Exception => e
|
95
79
|
if e.respond_to?(:record)
|
96
80
|
inherit_errors(e.record) unless e.record == self
|
@@ -127,18 +111,23 @@ module Subroutine
|
|
127
111
|
raise NotImplementedError
|
128
112
|
end
|
129
113
|
|
130
|
-
|
131
|
-
# returns false so failure cases can end with this invocation
|
132
|
-
def inherit_errors(error_object)
|
114
|
+
def inherit_errors(error_object, prefix: nil)
|
133
115
|
error_object = error_object.errors if error_object.respond_to?(:errors)
|
134
116
|
|
135
|
-
error_object.each do |
|
136
|
-
if
|
137
|
-
|
138
|
-
|
139
|
-
|
117
|
+
error_object.each do |field_name, error|
|
118
|
+
field_name = "#{prefix}#{field_name}" if prefix
|
119
|
+
field_name = field_name.to_sym
|
120
|
+
|
121
|
+
field_config = get_field_config(field_name)
|
122
|
+
field_config ||= begin
|
123
|
+
kv = field_configurations.find { |_k, config| config[:aka] == field_name }
|
124
|
+
kv ? kv.last : nil
|
125
|
+
end
|
126
|
+
|
127
|
+
if field_config
|
128
|
+
errors.add(field_config.field_name, error)
|
140
129
|
else
|
141
|
-
errors.add(:base, error_object.full_message(
|
130
|
+
errors.add(:base, error_object.full_message(field_name, error))
|
142
131
|
end
|
143
132
|
end
|
144
133
|
|
@@ -63,6 +63,15 @@ end
|
|
63
63
|
String(value)
|
64
64
|
end
|
65
65
|
|
66
|
+
::Subroutine::TypeCaster.register :foreign_key do |value, options = {}|
|
67
|
+
next nil if value.blank?
|
68
|
+
|
69
|
+
next ::Subroutine::TypeCaster.cast(value, type: options[:foreign_key_type]) if options[:foreign_key_type]
|
70
|
+
next ::Subroutine::TypeCaster.cast(value, type: :integer) if options[:name] && options[:name].to_s.end_with?("_id")
|
71
|
+
|
72
|
+
value
|
73
|
+
end
|
74
|
+
|
66
75
|
::Subroutine::TypeCaster.register :boolean, :bool do |value, _options = {}|
|
67
76
|
!!(String(value) =~ /^(yes|true|1|ok)$/)
|
68
77
|
end
|
data/lib/subroutine/version.rb
CHANGED
data/subroutine.gemspec
CHANGED
@@ -1,15 +1,16 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
5
|
+
require "subroutine/version"
|
5
6
|
|
6
7
|
Gem::Specification.new do |spec|
|
7
8
|
spec.name = "subroutine"
|
8
9
|
spec.version = Subroutine::VERSION
|
9
10
|
spec.authors = ["Mike Nelson"]
|
10
11
|
spec.email = ["mike@mnelson.io"]
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
12
|
+
spec.summary = "Feature-driven operation objects."
|
13
|
+
spec.description = "An interface for creating feature-driven operations."
|
13
14
|
spec.homepage = "https://github.com/mnelson/subroutine"
|
14
15
|
spec.license = "MIT"
|
15
16
|
|
@@ -19,12 +20,14 @@ Gem::Specification.new do |spec|
|
|
19
20
|
spec.require_paths = ["lib"]
|
20
21
|
|
21
22
|
spec.add_dependency "activemodel", ">= 4.0.0"
|
23
|
+
spec.add_dependency "activesupport", ">= 4.0.0"
|
22
24
|
|
25
|
+
spec.add_development_dependency "actionpack", ">= 4.0"
|
23
26
|
spec.add_development_dependency "bundler"
|
24
|
-
spec.add_development_dependency "
|
27
|
+
spec.add_development_dependency "byebug"
|
25
28
|
spec.add_development_dependency "m"
|
26
29
|
spec.add_development_dependency "minitest"
|
27
30
|
spec.add_development_dependency "minitest-reporters"
|
28
31
|
spec.add_development_dependency "mocha"
|
29
|
-
spec.add_development_dependency "
|
32
|
+
spec.add_development_dependency "rake"
|
30
33
|
end
|
@@ -23,11 +23,21 @@ module Subroutine
|
|
23
23
|
assert_equal doug.id, op.user_id
|
24
24
|
end
|
25
25
|
|
26
|
+
def test_it_can_be_nil
|
27
|
+
op = SimpleAssociationOp.new user: nil
|
28
|
+
assert_nil op.user
|
29
|
+
assert_nil op.user_id
|
30
|
+
|
31
|
+
op = SimpleAssociationOp.new
|
32
|
+
assert_nil op.user
|
33
|
+
assert_nil op.user_id
|
34
|
+
end
|
35
|
+
|
26
36
|
def test_it_looks_up_an_association
|
27
37
|
all_mock = mock
|
28
38
|
|
29
39
|
::User.expects(:all).returns(all_mock)
|
30
|
-
all_mock.expects(:
|
40
|
+
all_mock.expects(:find_by!).with(id: 1).returns(doug)
|
31
41
|
|
32
42
|
op = SimpleAssociationOp.new user_type: "User", user_id: doug.id
|
33
43
|
assert_equal doug, op.user
|
@@ -37,7 +47,7 @@ module Subroutine
|
|
37
47
|
all_mock = mock
|
38
48
|
|
39
49
|
::User.expects(:all).returns(all_mock)
|
40
|
-
all_mock.expects(:
|
50
|
+
all_mock.expects(:find_by!).with(id: 1).returns(doug)
|
41
51
|
|
42
52
|
op = SimpleAssociationOp.new user_id: doug.id
|
43
53
|
assert_equal doug, op.user
|
@@ -49,7 +59,7 @@ module Subroutine
|
|
49
59
|
|
50
60
|
::User.expects(:all).returns(all_mock)
|
51
61
|
all_mock.expects(:unscoped).returns(unscoped_mock)
|
52
|
-
unscoped_mock.expects(:
|
62
|
+
unscoped_mock.expects(:find_by!).with(id: 1).returns(doug)
|
53
63
|
|
54
64
|
op = UnscopedSimpleAssociationOp.new user_id: doug.id
|
55
65
|
assert_equal doug, op.user
|
@@ -59,7 +69,7 @@ module Subroutine
|
|
59
69
|
all_mock = mock
|
60
70
|
::User.expects(:all).never
|
61
71
|
::AdminUser.expects(:all).returns(all_mock)
|
62
|
-
all_mock.expects(:
|
72
|
+
all_mock.expects(:find_by!).with(id: 1).returns(doug)
|
63
73
|
|
64
74
|
op = PolymorphicAssociationOp.new(admin_type: "AdminUser", admin_id: doug.id)
|
65
75
|
assert_equal doug, op.admin
|
@@ -70,11 +80,72 @@ module Subroutine
|
|
70
80
|
assert_equal "AdminUser", op.admin_type
|
71
81
|
end
|
72
82
|
|
83
|
+
def test_it_allows_foreign_key_to_be_set
|
84
|
+
all_mock = mock
|
85
|
+
::User.expects(:all).returns(all_mock)
|
86
|
+
all_mock.expects(:find_by!).with(id: 10).returns(doug)
|
87
|
+
|
88
|
+
op = ::AssociationWithForeignKeyOp.new(owner_id: 10)
|
89
|
+
assert_equal doug, op.user
|
90
|
+
assert_equal "owner_id", op.field_configurations[:user][:foreign_key]
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_the_foreign_key_is_cast
|
94
|
+
all_mock = mock
|
95
|
+
::User.expects(:all).returns(all_mock)
|
96
|
+
all_mock.expects(:find_by!).with(id: 10).returns(doug)
|
97
|
+
|
98
|
+
op = ::AssociationWithForeignKeyOp.new(owner_id: "10")
|
99
|
+
assert_equal doug, op.user
|
100
|
+
assert_equal 10, op.owner_id
|
101
|
+
assert_equal "owner_id", op.field_configurations[:user][:foreign_key]
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_it_allows_a_foreign_key_and_find_by_to_be_set
|
105
|
+
all_mock = mock
|
106
|
+
::User.expects(:all).returns(all_mock)
|
107
|
+
all_mock.expects(:find_by!).with(email_address: "foo@bar.com").returns(doug)
|
108
|
+
|
109
|
+
op = ::AssociationWithFindByAndForeignKeyOp.new(email_address: "foo@bar.com")
|
110
|
+
assert_equal doug, op.user
|
111
|
+
assert_equal "foo@bar.com", op.email_address
|
112
|
+
assert_equal "email_address", op.field_configurations[:user][:find_by]
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_it_allows_a_find_by_to_be_set
|
116
|
+
all_mock = mock
|
117
|
+
::User.expects(:all).returns(all_mock)
|
118
|
+
all_mock.expects(:find_by!).with(email_address: doug.email_address).returns(doug)
|
119
|
+
|
120
|
+
op = ::AssociationWithFindByKeyOp.new(user_id: doug.email_address)
|
121
|
+
assert_equal doug, op.user
|
122
|
+
assert_equal "email_address", op.field_configurations[:user][:find_by]
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_values_are_correct_for_find_by_usage
|
126
|
+
op = ::AssociationWithFindByKeyOp.new(user: doug)
|
127
|
+
assert_equal doug, op.user
|
128
|
+
assert_equal doug.email_address, op.user_id
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_values_are_correct_for_foreign_key_usage
|
132
|
+
op = ::AssociationWithForeignKeyOp.new(user: doug)
|
133
|
+
assert_equal doug, op.user
|
134
|
+
assert_equal doug.id, op.owner_id
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_values_are_correct_for_both_foreign_key_and_find_by_usage
|
138
|
+
op = ::AssociationWithFindByAndForeignKeyOp.new(user: doug)
|
139
|
+
assert_equal doug, op.user
|
140
|
+
assert_equal doug.email_address, op.email_address
|
141
|
+
assert_equal false, op.respond_to?(:user_id)
|
142
|
+
end
|
143
|
+
|
73
144
|
def test_it_inherits_associations_via_fields_from
|
74
145
|
all_mock = mock
|
75
146
|
|
76
147
|
::User.expects(:all).returns(all_mock)
|
77
|
-
all_mock.expects(:
|
148
|
+
all_mock.expects(:find_by!).with(id: 1).returns(doug)
|
78
149
|
|
79
150
|
op = ::InheritedSimpleAssociation.new(user_type: "User", user_id: doug.id)
|
80
151
|
assert_equal doug, op.user
|
@@ -88,7 +159,7 @@ module Subroutine
|
|
88
159
|
|
89
160
|
::User.expects(:all).returns(all_mock)
|
90
161
|
all_mock.expects(:unscoped).returns(unscoped_mock)
|
91
|
-
unscoped_mock.expects(:
|
162
|
+
unscoped_mock.expects(:find_by!).with(id: 1).returns(doug)
|
92
163
|
|
93
164
|
op = ::InheritedUnscopedAssociation.new(user_type: "User", user_id: doug.id)
|
94
165
|
assert_equal doug, op.user
|
@@ -100,7 +171,7 @@ module Subroutine
|
|
100
171
|
all_mock = mock
|
101
172
|
::User.expects(:all).never
|
102
173
|
::AdminUser.expects(:all).returns(all_mock)
|
103
|
-
all_mock.expects(:
|
174
|
+
all_mock.expects(:find_by!).with(id: 1).returns(doug)
|
104
175
|
|
105
176
|
op = ::InheritedPolymorphicAssociationOp.new(admin_type: "AdminUser", admin_id: doug.id)
|
106
177
|
assert_equal doug, op.admin
|
@@ -118,5 +118,17 @@ module Subroutine
|
|
118
118
|
RequireUserOp.new(user.id)
|
119
119
|
end
|
120
120
|
|
121
|
+
def test_actioncontroller_parameters_can_be_provided
|
122
|
+
raw_params = { some_input: "foobarbaz" }.with_indifferent_access
|
123
|
+
params = ::ActionController::Parameters.new(raw_params)
|
124
|
+
op = RequireUserOp.new(user, params)
|
125
|
+
op.submit!
|
126
|
+
|
127
|
+
assert_equal "foobarbaz", op.some_input
|
128
|
+
|
129
|
+
assert_equal raw_params, op.params
|
130
|
+
assert_equal user, op.current_user
|
131
|
+
end
|
132
|
+
|
121
133
|
end
|
122
134
|
end
|
@@ -42,6 +42,13 @@ module Subroutine
|
|
42
42
|
assert_equal [:baz], op.field_configurations.keys.sort
|
43
43
|
end
|
44
44
|
|
45
|
+
def test_fields_from_ignores_except_association_fields
|
46
|
+
op = ::ExceptAssociationOp.new
|
47
|
+
refute op.field_configurations.key?(:admin)
|
48
|
+
refute op.field_configurations.key?(:admin_id)
|
49
|
+
refute op.field_configurations.key?(:admin_type)
|
50
|
+
end
|
51
|
+
|
45
52
|
def test_fields_from_only_fields
|
46
53
|
op = ::OnlyFooBarOp.new
|
47
54
|
assert op.field_configurations.key?(:foo)
|
@@ -49,6 +56,13 @@ module Subroutine
|
|
49
56
|
refute_equal [:baz], op.field_configurations.keys.sort
|
50
57
|
end
|
51
58
|
|
59
|
+
def test_fields_from_only_association_fields
|
60
|
+
op = ::OnlyAssociationOp.new
|
61
|
+
assert op.field_configurations.key?(:admin)
|
62
|
+
assert op.field_configurations.key?(:admin_type)
|
63
|
+
assert op.field_configurations.key?(:admin_id)
|
64
|
+
end
|
65
|
+
|
52
66
|
def test_defaults_declaration_options
|
53
67
|
op = ::DefaultsOp.new
|
54
68
|
assert_equal "foo", op.foo
|
@@ -113,6 +127,13 @@ module Subroutine
|
|
113
127
|
assert_equal ["has gotta be @admin.com"], op.errors[:email]
|
114
128
|
end
|
115
129
|
|
130
|
+
def test_validation_errors_can_be_inherited_and_prefixed
|
131
|
+
op = PrefixedInputsOp.new(user_email_address: "foo@bar.com")
|
132
|
+
refute op.submit
|
133
|
+
|
134
|
+
assert_equal ["has gotta be @admin.com"], op.errors[:user_email_address]
|
135
|
+
end
|
136
|
+
|
116
137
|
def test_when_valid_perform_completes_it_returns_control
|
117
138
|
op = ::SignupOp.new(email: "foo@bar.com", password: "password123")
|
118
139
|
op.submit!
|
@@ -125,6 +146,11 @@ module Subroutine
|
|
125
146
|
assert_equal "foo@bar.com", u.email_address
|
126
147
|
end
|
127
148
|
|
149
|
+
def test_instance_and_class_submit_bang_return_instance
|
150
|
+
assert ::SignupOp.new(email: "foo@bar.com", password: "password123").submit!.is_a?(::SignupOp)
|
151
|
+
assert ::SignupOp.submit!(email: "foo@bar.com", password: "password123").is_a?(::SignupOp)
|
152
|
+
end
|
153
|
+
|
128
154
|
def test_it_raises_an_error_when_used_with_a_bang_and_performing_or_validation_fails
|
129
155
|
op = ::SignupOp.new(email: "foo@bar.com")
|
130
156
|
|
@@ -304,5 +330,17 @@ module Subroutine
|
|
304
330
|
end
|
305
331
|
end
|
306
332
|
|
333
|
+
def test_actioncontroller_parameters_can_be_provided
|
334
|
+
raw_params = { email: "foo@bar.com", password: "password123!" }.with_indifferent_access
|
335
|
+
params = ::ActionController::Parameters.new(raw_params)
|
336
|
+
op = SignupOp.new(params)
|
337
|
+
op.submit!
|
338
|
+
|
339
|
+
assert_equal "foo@bar.com", op.email
|
340
|
+
assert_equal "password123!", op.password
|
341
|
+
|
342
|
+
assert_equal raw_params, op.params
|
343
|
+
end
|
344
|
+
|
307
345
|
end
|
308
346
|
end
|
@@ -261,5 +261,28 @@ module Subroutine
|
|
261
261
|
op.date_input = "2015-13-01"
|
262
262
|
end
|
263
263
|
end
|
264
|
+
|
265
|
+
def test_foreign_key_inputs
|
266
|
+
op.fk_input_owner_id = nil
|
267
|
+
assert_nil op.fk_input_owner_id
|
268
|
+
|
269
|
+
op.fk_input_owner_id = ""
|
270
|
+
assert_nil op.fk_input_owner_id
|
271
|
+
|
272
|
+
op.fk_input_owner_id = "19402"
|
273
|
+
assert_equal 19402, op.fk_input_owner_id
|
274
|
+
|
275
|
+
op.fk_input_owner_id = "19402.0"
|
276
|
+
assert_equal 19402, op.fk_input_owner_id
|
277
|
+
|
278
|
+
op.fk_input_email_address = nil
|
279
|
+
assert_nil op.fk_input_email_address
|
280
|
+
|
281
|
+
op.fk_input_email_address = ""
|
282
|
+
assert_nil op.fk_input_email_address
|
283
|
+
|
284
|
+
op.fk_input_email_address = "foo@bar.com"
|
285
|
+
assert_equal "foo@bar.com", op.fk_input_email_address
|
286
|
+
end
|
264
287
|
end
|
265
288
|
end
|
data/test/support/ops.rb
CHANGED
@@ -23,6 +23,14 @@ class User
|
|
23
23
|
new(id: id)
|
24
24
|
end
|
25
25
|
|
26
|
+
def self.find_by(params)
|
27
|
+
new(params)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.find_by!(params)
|
31
|
+
find_by(params) || raise
|
32
|
+
end
|
33
|
+
|
26
34
|
end
|
27
35
|
|
28
36
|
class AdminUser < ::User
|
@@ -158,6 +166,8 @@ class TypeCastOp < ::Subroutine::Op
|
|
158
166
|
array :array_input, default: "foo"
|
159
167
|
array :type_array_input, of: :integer
|
160
168
|
file :file_input
|
169
|
+
foreign_key :fk_input_owner_id
|
170
|
+
foreign_key :fk_input_email_address, foreign_key_type: :string
|
161
171
|
|
162
172
|
end
|
163
173
|
|
@@ -177,6 +187,8 @@ class RequireUserOp < OpWithAuth
|
|
177
187
|
|
178
188
|
require_user!
|
179
189
|
|
190
|
+
string :some_input
|
191
|
+
|
180
192
|
end
|
181
193
|
|
182
194
|
class RequireNoUserOp < OpWithAuth
|
@@ -206,7 +218,7 @@ class CustomAuthorizeOp < OpWithAuth
|
|
206
218
|
protected
|
207
219
|
|
208
220
|
def authorize_user_is_correct
|
209
|
-
unauthorized! unless current_user.email_address.to_s =~ /example\.com$/
|
221
|
+
unauthorized! unless current_user.email_address.to_s =~ /example\.com$/ # rubocop:disable Performance/RegexpMatch
|
210
222
|
end
|
211
223
|
|
212
224
|
end
|
@@ -302,6 +314,36 @@ class AssociationWithClassOp < ::OpWithAssociation
|
|
302
314
|
|
303
315
|
end
|
304
316
|
|
317
|
+
class AssociationWithForeignKeyOp < ::OpWithAssociation
|
318
|
+
|
319
|
+
association :user, foreign_key: "owner_id"
|
320
|
+
|
321
|
+
end
|
322
|
+
|
323
|
+
class AssociationWithFindByKeyOp < ::OpWithAssociation
|
324
|
+
|
325
|
+
association :user, find_by: "email_address", foreign_key_type: :string
|
326
|
+
|
327
|
+
end
|
328
|
+
|
329
|
+
class AssociationWithFindByAndForeignKeyOp < ::OpWithAssociation
|
330
|
+
|
331
|
+
association :user, foreign_key: "email_address", find_by: "email_address"
|
332
|
+
|
333
|
+
end
|
334
|
+
|
335
|
+
class ExceptAssociationOp < ::Subroutine::Op
|
336
|
+
|
337
|
+
fields_from ::PolymorphicAssociationOp, except: %i[admin]
|
338
|
+
|
339
|
+
end
|
340
|
+
|
341
|
+
class OnlyAssociationOp < ::Subroutine::Op
|
342
|
+
|
343
|
+
fields_from ::PolymorphicAssociationOp, only: %i[admin]
|
344
|
+
|
345
|
+
end
|
346
|
+
|
305
347
|
class InheritedSimpleAssociation < ::Subroutine::Op
|
306
348
|
|
307
349
|
fields_from SimpleAssociationOp
|
@@ -433,3 +475,15 @@ class CustomFailureClassOp < ::Subroutine::Op
|
|
433
475
|
end
|
434
476
|
|
435
477
|
end
|
478
|
+
|
479
|
+
class PrefixedInputsOp < ::Subroutine::Op
|
480
|
+
|
481
|
+
string :user_email_address
|
482
|
+
|
483
|
+
def perform
|
484
|
+
u = AdminUser.new(email_address: user_email_address)
|
485
|
+
u.valid?
|
486
|
+
inherit_errors(u, prefix: :user_)
|
487
|
+
end
|
488
|
+
|
489
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "subroutine"
|
4
|
+
require "minitest/autorun"
|
5
|
+
require "minitest/unit"
|
6
6
|
|
7
|
-
require
|
8
|
-
require
|
7
|
+
require "minitest/reporters"
|
8
|
+
require "mocha/minitest"
|
9
9
|
|
10
10
|
require "byebug"
|
11
|
+
require "action_controller"
|
11
12
|
|
12
13
|
Minitest::Reporters.use!([Minitest::Reporters::DefaultReporter.new])
|
13
14
|
|
14
15
|
class TestCase < ::Minitest::Test; end
|
15
16
|
|
16
|
-
require_relative
|
17
|
+
require_relative "support/ops"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: subroutine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0.beta2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Nelson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 4.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 4.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 4.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: actionpack
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.0'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: bundler
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -39,7 +67,7 @@ dependencies:
|
|
39
67
|
- !ruby/object:Gem::Version
|
40
68
|
version: '0'
|
41
69
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
70
|
+
name: byebug
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
44
72
|
requirements:
|
45
73
|
- - ">="
|
@@ -109,7 +137,7 @@ dependencies:
|
|
109
137
|
- !ruby/object:Gem::Version
|
110
138
|
version: '0'
|
111
139
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
140
|
+
name: rake
|
113
141
|
requirement: !ruby/object:Gem::Requirement
|
114
142
|
requirements:
|
115
143
|
- - ">="
|
@@ -133,6 +161,7 @@ files:
|
|
133
161
|
- ".ruby-gemset"
|
134
162
|
- ".ruby-version"
|
135
163
|
- ".travis.yml"
|
164
|
+
- CHANGELOG.MD
|
136
165
|
- Gemfile
|
137
166
|
- LICENSE.txt
|
138
167
|
- README.md
|
@@ -189,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
189
218
|
- !ruby/object:Gem::Version
|
190
219
|
version: 1.3.1
|
191
220
|
requirements: []
|
192
|
-
rubygems_version: 3.
|
221
|
+
rubygems_version: 3.3.7
|
193
222
|
signing_key:
|
194
223
|
specification_version: 4
|
195
224
|
summary: Feature-driven operation objects.
|