subroutine 0.10.0.beta → 0.10.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.
@@ -6,7 +6,8 @@ require "active_support/core_ext/hash/indifferent_access"
6
6
  require "active_support/core_ext/object/deep_dup"
7
7
 
8
8
  require "subroutine/type_caster"
9
- require "subroutine/association"
9
+ require "subroutine/fields/configuration"
10
+ require "subroutine/fields/mass_assignment_error"
10
11
 
11
12
  module Subroutine
12
13
  module Fields
@@ -14,24 +15,41 @@ module Subroutine
14
15
  extend ActiveSupport::Concern
15
16
 
16
17
  included do
17
- class_attribute :_fields
18
- self._fields = {}
19
- attr_reader :original_params
20
- attr_reader :params, :defaults
18
+ class_attribute :field_configurations
19
+ self.field_configurations = {}
21
20
  end
22
21
 
23
22
  module ClassMethods
24
- # fields can be provided in the following way:
25
- # field :field1, :field2
26
- # field :field3, :field4, default: 'my default'
27
- def field(*fields)
28
- options = fields.extract_options!
29
-
30
- fields.each do |f|
31
- _field(f, options)
23
+
24
+ def field(field_name, options = {})
25
+ config = ::Subroutine::Fields::Configuration.from(field_name, options)
26
+ config.validate!
27
+
28
+ config.groups.each do |group_name|
29
+ _group(group_name)
30
+ end
31
+
32
+ self.field_configurations = field_configurations.merge(field_name.to_sym => config)
33
+
34
+ if config.field_writer?
35
+ class_eval <<-EV, __FILE__, __LINE__ + 1
36
+ try(:silence_redefinition_of_method, :#{field_name}=)
37
+ def #{field_name}=(v)
38
+ set_field(:#{field_name}, v)
39
+ end
40
+ EV
41
+ end
42
+
43
+ if config.field_reader?
44
+ class_eval <<-EV, __FILE__, __LINE__ + 1
45
+ try(:silence_redefinition_of_method, :#{field_name})
46
+ def #{field_name}
47
+ get_field(:#{field_name})
48
+ end
49
+ EV
32
50
  end
33
51
  end
34
- alias_method :fields, :field
52
+ alias input field
35
53
 
36
54
  def inputs_from(*things)
37
55
  options = things.extract_options!
@@ -39,20 +57,31 @@ module Subroutine
39
57
  onlys = options.key?(:only) ? Array(options.delete(:only)) : nil
40
58
 
41
59
  things.each do |thing|
42
- thing._fields.each_pair do |field_name, opts|
43
- next if excepts && excepts.include?(field_name)
60
+ thing.field_configurations.each_pair do |field_name, config|
61
+ next if excepts&.include?(field_name)
44
62
  next if onlys && !onlys.include?(field_name)
45
63
 
46
- if opts[:association]
47
- include ::Subroutine::Association unless included_modules.include?(::Subroutine::Association)
48
- association(field_name, opts)
49
- else
50
- field(field_name, opts)
64
+ config.required_modules.each do |mod|
65
+ include mod unless included_modules.include?(mod)
51
66
  end
67
+
68
+ field(field_name, config)
52
69
  end
53
70
  end
54
71
  end
55
- alias_method :fields_from, :inputs_from
72
+ alias fields_from inputs_from
73
+
74
+ def fields_in_group(group_name)
75
+ field_configurations.each_with_object({}) do |(field_name, config), h|
76
+ next unless config.in_group?(group_name)
77
+
78
+ h[field_name] = config
79
+ end
80
+ end
81
+
82
+ def get_field_config(field_name)
83
+ field_configurations[field_name.to_sym]
84
+ end
56
85
 
57
86
  def respond_to_missing?(method_name, *args, &block)
58
87
  ::Subroutine::TypeCaster.casters.key?(method_name.to_sym) || super
@@ -61,10 +90,10 @@ module Subroutine
61
90
  def method_missing(method_name, *args, &block)
62
91
  caster = ::Subroutine::TypeCaster.casters[method_name.to_sym]
63
92
  if caster
64
- options = args.extract_options!
93
+ field_name, options = args
94
+ options ||= {}
65
95
  options[:type] = method_name.to_sym
66
- args.push(options)
67
- field(*args, &block)
96
+ field(field_name, options)
68
97
  else
69
98
  super
70
99
  end
@@ -72,84 +101,106 @@ module Subroutine
72
101
 
73
102
  protected
74
103
 
75
- def _field(field_name, field_writer: true, field_reader: true, **options)
76
- self._fields = _fields.merge(field_name.to_sym => options)
77
-
78
- if field_writer
79
- class_eval <<-EV, __FILE__, __LINE__ + 1
80
- try(:silence_redefinition_of_method, :#{field_name}=)
81
- def #{field_name}=(v)
82
- config = #{field_name}_config
83
- @fields_provided["#{field_name}"] = true
84
- @params["#{field_name}"] = attempt_cast(v, config) do |e|
85
- "Error during assignment of field `#{field_name}`: \#{e}"
86
- end
87
- end
88
- EV
89
- end
90
-
91
- if field_reader
92
- class_eval <<-EV, __FILE__, __LINE__ + 1
93
- try(:silence_redefinition_of_method, :#{field_name})
94
- def #{field_name}
95
- @params["#{field_name}"]
96
- end
97
- EV
98
- end
99
-
104
+ def _group(group_name)
100
105
  class_eval <<-EV, __FILE__, __LINE__ + 1
101
- try(:silence_redefinition_of_method, :#{field_name}_config)
102
- def #{field_name}_config
103
- _fields[:#{field_name}]
106
+ try(:silence_redefinition_of_method, :#{group_name}_params)
107
+ def #{group_name}_params
108
+ param_groups[:#{group_name}]
109
+ end
110
+
111
+ try(:silence_redefinition_of_method, :without_#{group_name}_params)
112
+ def without_#{group_name}_params
113
+ all_params.except(*#{group_name}_params.keys)
104
114
  end
105
115
  EV
106
116
  end
117
+
107
118
  end
108
119
 
109
120
  def setup_fields(inputs = {})
110
- @original_params = inputs.with_indifferent_access
111
- @defaults = build_defaults
112
- @fields_provided = {}.with_indifferent_access
113
- @params = build_params(@original_params, @defaults)
121
+ @provided_fields = {}.with_indifferent_access
122
+ param_groups[:original] = inputs.with_indifferent_access
123
+ param_groups[:default] = build_defaults
124
+ mass_assign_initial_params
125
+ end
126
+
127
+ def param_groups
128
+ @param_groups ||= Hash.new { |h, k| h[k] = {}.with_indifferent_access }
129
+ end
130
+
131
+ def get_param_group(name)
132
+ param_groups[name.to_sym]
133
+ end
134
+
135
+ def original_params
136
+ get_param_group(:original)
137
+ end
138
+
139
+ def ungrouped_params
140
+ get_param_group(:ungrouped)
141
+ end
142
+ alias params ungrouped_params
143
+
144
+ def all_params
145
+ get_param_group(:all)
146
+ end
147
+
148
+ def defaults
149
+ get_param_group(:default)
150
+ end
151
+ alias default_params defaults
152
+
153
+ def get_field_config(field_name)
154
+ self.class.get_field_config(field_name)
114
155
  end
115
156
 
116
157
  # check if a specific field was provided
117
158
  def field_provided?(key)
118
- return send(:"#{key}_field_provided?") if respond_to?(:"#{key}_field_provided?", true)
159
+ !!@provided_fields[key]
160
+ end
119
161
 
120
- !!@fields_provided[key]
162
+ def get_field(name)
163
+ all_params[name]
121
164
  end
122
165
 
123
- # if you want to use strong parameters or something in your form object you can do so here.
124
- # by default we just slice the inputs to the defined fields
125
- def build_params(inputs, defaults)
126
- out = {}.with_indifferent_access
166
+ def set_field(name, value, track_provided: true)
167
+ config = get_field_config(name)
168
+ @provided_fields[name] = true if track_provided
169
+ value = attempt_cast(value, config) do |e|
170
+ "Error during assignment of field `#{name}`: #{e}"
171
+ end
172
+ each_param_group_for_field(name) do |h|
173
+ h[name] = value
174
+ end
175
+ value
176
+ end
127
177
 
128
- _fields.each_pair do |field, config|
178
+ def clear_field(name)
179
+ each_param_group_for_field(name) do |h|
180
+ h.delete(name)
181
+ end
182
+ end
183
+
184
+ protected
129
185
 
130
- if config[:mass_assignable] == false && inputs.key?(field)
131
- raise ArgumentError, "`#{field}` is not mass assignable"
186
+ def mass_assign_initial_params
187
+ field_configurations.each_pair do |field_name, config|
188
+ if !config.mass_assignable? && original_params.key?(field_name)
189
+ raise ::Subroutine::Fields::MassAssignmentError, field_name
132
190
  end
133
191
 
134
- if inputs.key?(field)
135
- @fields_provided[field] = true
136
- out[field] = attempt_cast(inputs[field], config) do |e|
137
- "Error for field `#{field}`: #{e}"
138
- end
139
- elsif defaults.key?(field)
140
- out[field] = defaults[field]
141
- else
142
- next
192
+ if original_params.key?(field_name)
193
+ set_field(field_name, original_params[field_name])
194
+ elsif defaults.key?(field_name)
195
+ set_field(field_name, defaults[field_name], track_provided: false)
143
196
  end
144
197
  end
145
-
146
- out
147
198
  end
148
199
 
149
200
  def build_defaults
150
- @defaults = {}.with_indifferent_access
201
+ out = {}.with_indifferent_access
151
202
 
152
- _fields.each_pair do |field, config|
203
+ field_configurations.each_pair do |field, config|
153
204
  next unless config.key?(:default)
154
205
 
155
206
  deflt = config[:default]
@@ -161,20 +212,33 @@ module Subroutine
161
212
  deflt = deflt.deep_dup # from active_support
162
213
  end
163
214
 
164
- @defaults[field.to_s] = attempt_cast(deflt, config) do |e|
215
+ out[field.to_s] = attempt_cast(deflt, config) do |e|
165
216
  "Error for default `#{field}`: #{e}"
166
217
  end
167
218
  end
168
219
 
169
- @defaults
220
+ out
170
221
  end
171
222
 
172
223
  def attempt_cast(value, config)
173
224
  ::Subroutine::TypeCaster.cast(value, config)
174
- rescue ::Subroutine::TypeCaster::TypeCastError => e
225
+ rescue ::Subroutine::TypeCaster::TypeCastError => e
175
226
  message = block_given? ? yield(e) : e.to_s
176
227
  raise ::Subroutine::TypeCaster::TypeCastError, message, e.backtrace
177
228
  end
178
229
 
230
+ def each_param_group_for_field(name)
231
+ config = get_field_config(name)
232
+ yield all_params
233
+
234
+ if config.groups.empty?
235
+ yield ungrouped_params
236
+ else
237
+ config.groups.each do |group_name|
238
+ yield param_groups[group_name]
239
+ end
240
+ end
241
+ end
242
+
179
243
  end
180
244
  end
data/lib/subroutine/op.rb CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  require "active_model"
4
4
 
5
- require "subroutine/fields"
6
5
  require "subroutine/failure"
7
- require "subroutine/output_not_set_error"
8
- require "subroutine/unknown_output_error"
6
+ require "subroutine/fields"
7
+ require "subroutine/outputs"
9
8
 
10
9
  module Subroutine
11
10
  class Op
@@ -13,28 +12,17 @@ module Subroutine
13
12
  include ::ActiveModel::Validations
14
13
  include ::ActiveModel::Validations::Callbacks
15
14
  include ::Subroutine::Fields
16
-
17
- DEFAULT_OUTPUT_OPTIONS = {
18
- required: true,
19
- }.freeze
15
+ include ::Subroutine::Outputs
20
16
 
21
17
  class << self
22
18
 
23
- def outputs(*names)
24
- options = names.extract_options!
25
- names.each do |name|
26
- self._outputs = _outputs.merge(name.to_sym => DEFAULT_OUTPUT_OPTIONS.merge(options))
27
-
28
- class_eval <<-EV, __FILE__, __LINE__ + 1
29
- def #{name}
30
- @outputs[:#{name}]
31
- end
32
- EV
33
- end
19
+ def failure_class(klass)
20
+ self._failure_class = klass
34
21
  end
35
22
 
36
23
  def submit!(*args)
37
24
  raise ArgumentError, "Blocks cannot be provided to `submit!`" if block_given?
25
+
38
26
  op = new(*args)
39
27
  op.submit!
40
28
 
@@ -43,6 +31,7 @@ module Subroutine
43
31
 
44
32
  def submit(*args)
45
33
  raise ArgumentError, "Blocks cannot be provided to `submit`." if block_given?
34
+
46
35
  op = new(*args)
47
36
  op.submit
48
37
  op
@@ -50,7 +39,7 @@ module Subroutine
50
39
 
51
40
  protected
52
41
 
53
- def _field(field_name, options = {})
42
+ def field(field_name, options = {})
54
43
  result = super(field_name, options)
55
44
 
56
45
  if options[:aka]
@@ -64,26 +53,18 @@ module Subroutine
64
53
 
65
54
  end
66
55
 
67
- class_attribute :_outputs
68
- self._outputs = {}
56
+ class_attribute :_failure_class
57
+ self._failure_class = Subroutine::Failure
69
58
 
70
59
  class_attribute :_error_map
71
60
  self._error_map = {}
72
61
 
73
62
  def initialize(inputs = {})
74
63
  setup_fields(inputs)
75
- @outputs = {}
64
+ setup_outputs
76
65
  yield self if block_given?
77
66
  end
78
67
 
79
- def output(name, value)
80
- unless _outputs.key?(name.to_sym)
81
- raise ::Subroutine::UnknownOutputError, name
82
- end
83
-
84
- @outputs[name.to_sym] = value
85
- end
86
-
87
68
  def submit!
88
69
  begin
89
70
  observe_submission do
@@ -92,7 +73,7 @@ module Subroutine
92
73
  rescue Exception => e
93
74
  if e.respond_to?(:record)
94
75
  inherit_errors(e.record) unless e.record == self
95
- new_e = ::Subroutine::Failure.new(self)
76
+ new_e = _failure_class.new(self)
96
77
  raise new_e, new_e.message, e.backtrace
97
78
  else
98
79
  raise
@@ -100,15 +81,10 @@ module Subroutine
100
81
  end
101
82
 
102
83
  if errors.empty?
103
- _outputs.each_pair do |name, config|
104
- if config[:required] && !@outputs.key?(name)
105
- raise ::Subroutine::OutputNotSetError, name
106
- end
107
- end
108
-
84
+ validate_outputs!
109
85
  true
110
86
  else
111
- raise ::Subroutine::Failure, self
87
+ raise _failure_class, self
112
88
  end
113
89
  end
114
90
 
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ module Subroutine
6
+ module Outputs
7
+ class Configuration < ::SimpleDelegator
8
+
9
+ def self.from(field_name, options)
10
+ case options
11
+ when Subroutine::Outputs::Configuration
12
+ options.class.new(field_name, options)
13
+ else
14
+ new(field_name, options)
15
+ end
16
+ end
17
+
18
+ DEFAULT_OPTIONS = { required: true }.freeze
19
+
20
+ attr_reader :output_name
21
+
22
+ def initialize(output_name, config)
23
+ @output_name = output_name
24
+ super(DEFAULT_OPTIONS.merge(config))
25
+ end
26
+
27
+ alias config __getobj__
28
+
29
+ def required?
30
+ !!config[:required]
31
+ end
32
+
33
+ def inspect
34
+ "#<#{self.class}:#{object_id} name=#{output_name} config=#{config.inspect}>"
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Subroutine
4
+ module Outputs
5
+ class OutputNotSetError < StandardError
6
+
7
+ def initialize(name)
8
+ super("Expected output '#{name}' to be set upon completion of perform but was not.")
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Subroutine
4
+ module Outputs
5
+ class UnknownOutputError < StandardError
6
+
7
+ def initialize(name)
8
+ super("Unknown output '#{name}'")
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "subroutine/outputs/configuration"
5
+ require "subroutine/outputs/output_not_set_error"
6
+ require "subroutine/outputs/unknown_output_error"
7
+
8
+ module Subroutine
9
+ module Outputs
10
+
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ class_attribute :output_configurations
15
+ self.output_configurations = {}
16
+
17
+ attr_reader :outputs
18
+ end
19
+
20
+ module ClassMethods
21
+
22
+ def outputs(*names)
23
+ options = names.extract_options!
24
+ names.each do |name|
25
+ config = ::Subroutine::Outputs::Configuration.new(name, options)
26
+ self.output_configurations = output_configurations.merge(name.to_sym => config)
27
+
28
+ class_eval <<-EV, __FILE__, __LINE__ + 1
29
+ def #{name}
30
+ get_output(:#{name})
31
+ end
32
+ EV
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ def setup_outputs
39
+ @outputs = {}.with_indifferent_access
40
+ end
41
+
42
+ def output(name, value)
43
+ unless output_configurations.key?(name.to_sym)
44
+ raise ::Subroutine::Outputs::UnknownOutputError, name
45
+ end
46
+
47
+ outputs[name.to_sym] = value
48
+ end
49
+
50
+ def get_output(name)
51
+ name = name.to_sym
52
+ raise ::Subroutine::Outputs::UnknownOutputError, name unless output_configurations.key?(name)
53
+
54
+ outputs[name]
55
+ end
56
+
57
+ def validate_outputs!
58
+ output_configurations.each_pair do |name, config|
59
+ if config.required? && !outputs.key?(name)
60
+ raise ::Subroutine::Outputs::OutputNotSetError, name
61
+ end
62
+ end
63
+ end
64
+
65
+ end
66
+ end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Subroutine
4
+
4
5
  MAJOR = 0
5
6
  MINOR = 10
6
7
  PATCH = 0
7
- PRE = "beta"
8
+ PRE = "beta2"
9
+
10
+ VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
8
11
 
9
- VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join('.')
10
12
  end
@@ -1,20 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'test_helper'
3
+ require "test_helper"
4
4
 
5
5
  module Subroutine
6
- class AuthTest < TestCase
6
+ class AssociationTest < TestCase
7
+
7
8
  def doug
8
- @doug ||= ::User.new(id: 1, email_address: 'doug@example.com')
9
+ @doug ||= ::User.new(id: 1, email_address: "doug@example.com")
9
10
  end
10
11
 
11
12
  def fred
12
- @fred ||= ::User.new(id: 2, email_address: 'fred@example.com')
13
+ @fred ||= ::User.new(id: 2, email_address: "fred@example.com")
13
14
  end
14
15
 
15
16
  def test_it_sets_accessors_on_init
16
17
  op = SimpleAssociationOp.new user: doug
17
- assert_equal 'User', op.user_type
18
+ assert_equal "User", op.user_type
18
19
  assert_equal doug.id, op.user_id
19
20
  end
20
21
 
@@ -24,7 +25,7 @@ module Subroutine
24
25
  ::User.expects(:all).returns(all_mock)
25
26
  all_mock.expects(:find).with(1).returns(doug)
26
27
 
27
- op = SimpleAssociationOp.new user_type: 'User', user_id: doug.id
28
+ op = SimpleAssociationOp.new user_type: "User", user_id: doug.id
28
29
  assert_equal doug, op.user
29
30
  end
30
31
 
@@ -34,7 +35,7 @@ module Subroutine
34
35
  ::User.expects(:all).returns(all_mock)
35
36
  all_mock.expects(:find).with(1).returns(doug)
36
37
 
37
- op = SimpleAssociationOp.new user_type: 'users', user_id: doug.id
38
+ op = SimpleAssociationOp.new user_id: doug.id
38
39
  assert_equal doug, op.user
39
40
  end
40
41
 
@@ -46,7 +47,7 @@ module Subroutine
46
47
  all_mock.expects(:unscoped).returns(unscoped_mock)
47
48
  unscoped_mock.expects(:find).with(1).returns(doug)
48
49
 
49
- op = UnscopedSimpleAssociationOp.new user_type: 'User', user_id: doug.id
50
+ op = UnscopedSimpleAssociationOp.new user_id: doug.id
50
51
  assert_equal doug, op.user
51
52
  end
52
53
 
@@ -56,13 +57,13 @@ module Subroutine
56
57
  ::AdminUser.expects(:all).returns(all_mock)
57
58
  all_mock.expects(:find).with(1).returns(doug)
58
59
 
59
- op = PolymorphicAssociationOp.new(admin_type: 'AdminUser', admin_id: doug.id)
60
+ op = PolymorphicAssociationOp.new(admin_type: "AdminUser", admin_id: doug.id)
60
61
  assert_equal doug, op.admin
61
62
  end
62
63
 
63
64
  def test_it_allows_the_class_to_be_set
64
65
  op = ::AssociationWithClassOp.new(admin: doug)
65
- assert_equal 'AdminUser', op.admin_type
66
+ assert_equal "AdminUser", op.admin_type
66
67
  end
67
68
 
68
69
  def test_it_inherits_associations_via_inputs_from
@@ -71,9 +72,9 @@ module Subroutine
71
72
  ::User.expects(:all).returns(all_mock)
72
73
  all_mock.expects(:find).with(1).returns(doug)
73
74
 
74
- op = ::InheritedSimpleAssociation.new(user_type: 'User', user_id: doug.id)
75
+ op = ::InheritedSimpleAssociation.new(user_type: "User", user_id: doug.id)
75
76
  assert_equal doug, op.user
76
- assert_equal 'User', op.user_type
77
+ assert_equal "User", op.user_type
77
78
  assert_equal doug.id, op.user_id
78
79
  end
79
80
 
@@ -85,9 +86,9 @@ module Subroutine
85
86
  all_mock.expects(:unscoped).returns(unscoped_mock)
86
87
  unscoped_mock.expects(:find).with(1).returns(doug)
87
88
 
88
- op = ::InheritedUnscopedAssociation.new(user_type: 'User', user_id: doug.id)
89
+ op = ::InheritedUnscopedAssociation.new(user_type: "User", user_id: doug.id)
89
90
  assert_equal doug, op.user
90
- assert_equal 'User', op.user_type
91
+ assert_equal "User", op.user_type
91
92
  assert_equal doug.id, op.user_id
92
93
  end
93
94
 
@@ -97,9 +98,9 @@ module Subroutine
97
98
  ::AdminUser.expects(:all).returns(all_mock)
98
99
  all_mock.expects(:find).with(1).returns(doug)
99
100
 
100
- op = ::InheritedPolymorphicAssociationOp.new(admin_type: 'AdminUser', admin_id: doug.id)
101
+ op = ::InheritedPolymorphicAssociationOp.new(admin_type: "AdminUser", admin_id: doug.id)
101
102
  assert_equal doug, op.admin
102
- assert_equal 'AdminUser', op.admin_type
103
+ assert_equal "AdminUser", op.admin_type
103
104
  assert_equal doug.id, op.admin_id
104
105
  end
105
106
 
@@ -140,7 +141,7 @@ module Subroutine
140
141
  assert_equal false, op.field_provided?(:admin)
141
142
  assert_equal false, op.field_provided?(:admin_id)
142
143
  assert_equal false, op.field_provided?(:admin_type)
143
-
144
144
  end
145
+
145
146
  end
146
147
  end