subroutine 0.10.0.beta → 0.10.0.beta2

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