subroutine 0.0.1 → 0.1.0
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/.gitignore +1 -0
- data/README.md +2 -2
- data/lib/subroutine/failure.rb +12 -0
- data/lib/subroutine/op.rb +222 -0
- data/lib/subroutine/type_caster.rb +117 -0
- data/lib/subroutine/version.rb +2 -2
- data/lib/subroutine.rb +1 -242
- data/test/subroutine/base_test.rb +15 -29
- data/test/subroutine/type_caster_test.rb +224 -0
- data/test/support/ops.rb +20 -11
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55319d241f3bd11613cff48ead9bae0355dfe8fa
|
4
|
+
data.tar.gz: 147c53874dfa950fb987b31b4b9742f0f3a7e7f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 697e9e97d7161ac52db1c226ffa7f4e665d90d3c4f1b1fe58c1ae6e6d98555ae1a82538985b6b85c8690a4eef7385e74530068f054417cd34c45ff40a9a4f46c
|
7
|
+
data.tar.gz: 72303c67318d9aba48903ed4cf6ea9595adf22004f5af775f59a1df9d0082d08af1ed6954cd19b165d288669e58802b3ab1a391c30c0f36272d0e50e55e08d0f
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Subroutine
|
2
2
|
|
3
|
-
A gem that provides an interface for creating feature-driven operations. It
|
3
|
+
A gem that provides an interface for creating feature-driven operations. It loosely implements the command pattern if you're interested in nerding out a bit. See the examples below, it'll be more clear.
|
4
4
|
|
5
5
|
## Examples
|
6
6
|
|
@@ -114,7 +114,7 @@ end
|
|
114
114
|
|
115
115
|
## Usage
|
116
116
|
|
117
|
-
|
117
|
+
The `Subroutine::Op` class' `submit` and `submit!` methods have the same signature as the class' constructor, enabling a few different ways to utilize an op. Here they are:
|
118
118
|
|
119
119
|
#### Via the class' `submit` method
|
120
120
|
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
require 'active_model'
|
3
|
+
|
4
|
+
require "subroutine/failure"
|
5
|
+
require "subroutine/type_caster"
|
6
|
+
|
7
|
+
module Subroutine
|
8
|
+
|
9
|
+
class Op
|
10
|
+
|
11
|
+
include ::ActiveModel::Model
|
12
|
+
include ::ActiveModel::Validations::Callbacks
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
::Subroutine::TypeCaster::TYPES.values.flatten.each do |caster|
|
17
|
+
|
18
|
+
next if method_defined?(caster)
|
19
|
+
|
20
|
+
class_eval <<-EV, __FILE__, __LINE__ + 1
|
21
|
+
def #{caster}(*args)
|
22
|
+
options = args.extract_options!
|
23
|
+
options[:type] = #{caster.inspect}
|
24
|
+
args.push(options)
|
25
|
+
field(*args)
|
26
|
+
end
|
27
|
+
EV
|
28
|
+
end
|
29
|
+
|
30
|
+
# fields can be provided in the following way:
|
31
|
+
# field :field1, :field2
|
32
|
+
# field :field3, :field4, default: 'my default'
|
33
|
+
def field(*fields)
|
34
|
+
options = fields.extract_options!
|
35
|
+
|
36
|
+
fields.each do |f|
|
37
|
+
_field(f, options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
alias_method :fields, :field
|
42
|
+
|
43
|
+
|
44
|
+
def inputs_from(*ops)
|
45
|
+
ops.each do |op|
|
46
|
+
op._fields.each_pair do |field_name, options|
|
47
|
+
field(field_name, options)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def inherited(child)
|
53
|
+
super
|
54
|
+
child._fields = self._fields.dup
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def submit!(*args)
|
59
|
+
op = new(*args)
|
60
|
+
op.submit!
|
61
|
+
|
62
|
+
op
|
63
|
+
end
|
64
|
+
|
65
|
+
def submit(*args)
|
66
|
+
op = new(*args)
|
67
|
+
op.submit
|
68
|
+
op
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def _field(field_name, options = {})
|
74
|
+
self._fields[field_name.to_sym] = options
|
75
|
+
|
76
|
+
class_eval <<-EV, __FILE__, __LINE__ + 1
|
77
|
+
|
78
|
+
def #{field_name}=(v)
|
79
|
+
config = #{field_name}_config
|
80
|
+
@#{field_name} = type_caster.cast(v, config[:type])
|
81
|
+
end
|
82
|
+
|
83
|
+
def #{field_name}
|
84
|
+
return @#{field_name} if defined?(@#{field_name})
|
85
|
+
config = #{field_name}_config
|
86
|
+
deflt = config[:default]
|
87
|
+
deflt = deflt.call if deflt.respond_to?(:call)
|
88
|
+
type_caster.cast(deflt, config[:type])
|
89
|
+
end
|
90
|
+
|
91
|
+
def #{field_name}_config
|
92
|
+
self._fields[:#{field_name}]
|
93
|
+
end
|
94
|
+
|
95
|
+
EV
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
class_attribute :_fields
|
103
|
+
self._fields = {}
|
104
|
+
|
105
|
+
attr_reader :original_params
|
106
|
+
attr_reader :params
|
107
|
+
|
108
|
+
|
109
|
+
def initialize(inputs = {})
|
110
|
+
@original_params = inputs.with_indifferent_access
|
111
|
+
@params = {}
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
def submit!
|
116
|
+
unless submit
|
117
|
+
raise ::Subroutine::Failure.new(self)
|
118
|
+
end
|
119
|
+
true
|
120
|
+
end
|
121
|
+
|
122
|
+
# the action which should be invoked upon form submission (from the controller)
|
123
|
+
def submit
|
124
|
+
observe_submission do
|
125
|
+
@params = filter_params(@original_params)
|
126
|
+
|
127
|
+
set_accessors(@params)
|
128
|
+
|
129
|
+
validate_and_perform
|
130
|
+
end
|
131
|
+
|
132
|
+
rescue Exception => e
|
133
|
+
|
134
|
+
if e.respond_to?(:record)
|
135
|
+
inherit_errors_from(e.record) unless e.record == self
|
136
|
+
false
|
137
|
+
else
|
138
|
+
raise e
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
protected
|
143
|
+
|
144
|
+
def type_caster
|
145
|
+
@type_caster ||= ::Subroutine::TypeCaster.new
|
146
|
+
end
|
147
|
+
|
148
|
+
# these enable you to 1) add log output or 2) add performance monitoring such as skylight.
|
149
|
+
def observe_submission
|
150
|
+
yield
|
151
|
+
end
|
152
|
+
|
153
|
+
def observe_validation
|
154
|
+
yield
|
155
|
+
end
|
156
|
+
|
157
|
+
def observe_perform
|
158
|
+
yield
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
def validate_and_perform
|
163
|
+
bool = observe_validation do
|
164
|
+
valid?
|
165
|
+
end
|
166
|
+
|
167
|
+
return false unless bool
|
168
|
+
|
169
|
+
observe_perform do
|
170
|
+
perform
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# implement this in your concrete class.
|
175
|
+
def perform
|
176
|
+
raise NotImplementedError
|
177
|
+
end
|
178
|
+
|
179
|
+
# check if a specific field was provided
|
180
|
+
def field_provided?(key)
|
181
|
+
@params.has_key?(key)
|
182
|
+
end
|
183
|
+
|
184
|
+
# applies the errors to the form object from the child object
|
185
|
+
def inherit_errors_from(object)
|
186
|
+
inherit_errors(object.errors)
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
# applies the errors in error_object to self
|
191
|
+
# returns false so failure cases can end with this invocation
|
192
|
+
def inherit_errors(error_object)
|
193
|
+
error_object.each do |k,v|
|
194
|
+
|
195
|
+
if respond_to?("#{k}")
|
196
|
+
errors.add(k, v)
|
197
|
+
else
|
198
|
+
errors.add(:base, error_object.full_message(k,v))
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
false
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
# if you want to use strong parameters or something in your form object you can do so here.
|
208
|
+
# by default we just slice the inputs to the defined fields
|
209
|
+
def filter_params(inputs)
|
210
|
+
inputs.slice(*_fields.keys)
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
def set_accessors(inputs)
|
215
|
+
inputs.each do |key, value|
|
216
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'time'
|
3
|
+
require 'active_support/core_ext/object/blank'
|
4
|
+
require 'active_support/core_ext/object/try'
|
5
|
+
require 'active_support/core_ext/array/wrap'
|
6
|
+
|
7
|
+
module Subroutine
|
8
|
+
class TypeCaster
|
9
|
+
|
10
|
+
|
11
|
+
TYPES = {
|
12
|
+
:integer => [:int, :integer, :epoch],
|
13
|
+
:number => [:number, :float, :decimal],
|
14
|
+
:string => [:string, :text],
|
15
|
+
:boolean => [:bool, :boolean],
|
16
|
+
:iso_date => [:iso_date],
|
17
|
+
:iso_time => [:iso_time],
|
18
|
+
:date => [:date],
|
19
|
+
:time => [:time, :timestamp],
|
20
|
+
:hash => [:object, :hashmap, :dict],
|
21
|
+
:array => [:array]
|
22
|
+
}
|
23
|
+
|
24
|
+
|
25
|
+
def cast(value, type)
|
26
|
+
return value if value.nil? || type.nil?
|
27
|
+
|
28
|
+
case type.to_sym
|
29
|
+
when *TYPES[:integer]
|
30
|
+
cast_number(value).try(:to_i)
|
31
|
+
when *TYPES[:number]
|
32
|
+
cast_number(value)
|
33
|
+
when *TYPES[:string]
|
34
|
+
cast_string(value)
|
35
|
+
when *TYPES[:boolean]
|
36
|
+
cast_boolean(value)
|
37
|
+
when *TYPES[:iso_date]
|
38
|
+
t = cast_iso_time(value)
|
39
|
+
t ? t.split('T')[0] : t
|
40
|
+
when *TYPES[:date]
|
41
|
+
cast_date(value).try(:to_date)
|
42
|
+
when *TYPES[:iso_time]
|
43
|
+
cast_iso_time(value)
|
44
|
+
when *TYPES[:time]
|
45
|
+
cast_time(value)
|
46
|
+
when *TYPES[:hash]
|
47
|
+
cast_hash(value)
|
48
|
+
when *TYPES[:array]
|
49
|
+
cast_array(value)
|
50
|
+
else
|
51
|
+
value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def cast_number(value)
|
58
|
+
val = cast_string(value).strip
|
59
|
+
return nil if val.blank?
|
60
|
+
val.to_f
|
61
|
+
end
|
62
|
+
|
63
|
+
def cast_string(value)
|
64
|
+
String(value)
|
65
|
+
end
|
66
|
+
|
67
|
+
def cast_boolean(value)
|
68
|
+
!!(cast_string(value) =~ /^(yes|true|1|ok)$/)
|
69
|
+
end
|
70
|
+
|
71
|
+
def cast_time(value)
|
72
|
+
return nil unless value.present?
|
73
|
+
::Time.parse(cast_string(value))
|
74
|
+
end
|
75
|
+
|
76
|
+
def cast_date(value)
|
77
|
+
return nil unless value.present?
|
78
|
+
::Date.parse(cast_string(value))
|
79
|
+
end
|
80
|
+
|
81
|
+
def cast_iso_time(value)
|
82
|
+
return nil unless value.present?
|
83
|
+
t = nil
|
84
|
+
t ||= value if value.is_a?(::Time)
|
85
|
+
t ||= value if value.try(:acts_like?, :time)
|
86
|
+
t ||= ::Time.parse(cast_string(value))
|
87
|
+
t.utc.iso8601
|
88
|
+
end
|
89
|
+
|
90
|
+
def cast_iso_date(value)
|
91
|
+
return nil unless value.present?
|
92
|
+
d = nil
|
93
|
+
d ||= value if value.is_a?(::Date)
|
94
|
+
d ||= value if value.try(:acts_like?, :date)
|
95
|
+
d ||= ::Date.parse(cast_string(value))
|
96
|
+
d.iso8601
|
97
|
+
end
|
98
|
+
|
99
|
+
def cast_hash(value)
|
100
|
+
_cast_hash(value).try(:stringify_keys)
|
101
|
+
end
|
102
|
+
|
103
|
+
def _cast_hash(value)
|
104
|
+
return value if value.is_a?(Hash)
|
105
|
+
return {} if value.blank?
|
106
|
+
return value.to_h if value.respond_to?(:to_h)
|
107
|
+
return ::Hash[value.to_a] if value.respond_to?(:to_a)
|
108
|
+
{}
|
109
|
+
end
|
110
|
+
|
111
|
+
def cast_array(value)
|
112
|
+
return [] if value.blank?
|
113
|
+
::Array.wrap(value)
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
data/lib/subroutine/version.rb
CHANGED
data/lib/subroutine.rb
CHANGED
@@ -1,243 +1,2 @@
|
|
1
1
|
require "subroutine/version"
|
2
|
-
require
|
3
|
-
require 'active_model'
|
4
|
-
|
5
|
-
module Subroutine
|
6
|
-
|
7
|
-
class Failure < StandardError
|
8
|
-
attr_reader :record
|
9
|
-
def initialize(record)
|
10
|
-
@record = record
|
11
|
-
errors = @record.errors.full_messages.join(", ")
|
12
|
-
super(errors)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
class Op
|
17
|
-
|
18
|
-
include ::ActiveModel::Model
|
19
|
-
include ::ActiveModel::Validations::Callbacks
|
20
|
-
|
21
|
-
class << self
|
22
|
-
|
23
|
-
# fields can be provided in the following way:
|
24
|
-
# field :field1, :field2
|
25
|
-
# field :field3, :field4, default: 'my default'
|
26
|
-
# field field5: 'field5 default', field6: 'field6 default'
|
27
|
-
def field(*fields)
|
28
|
-
last_hash = fields.extract_options!
|
29
|
-
options = last_hash.slice(:default, :scope)
|
30
|
-
|
31
|
-
fields << last_hash.except(:default, :scope)
|
32
|
-
|
33
|
-
fields.each do |f|
|
34
|
-
|
35
|
-
if f.is_a?(Hash)
|
36
|
-
f.each do |k,v|
|
37
|
-
field(k, options.merge(:default => v))
|
38
|
-
end
|
39
|
-
else
|
40
|
-
|
41
|
-
_field(f, options)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
end
|
46
|
-
alias_method :fields, :field
|
47
|
-
|
48
|
-
|
49
|
-
def inputs_from(*ops)
|
50
|
-
ops.each do |op|
|
51
|
-
field(*op._fields)
|
52
|
-
defaults(op._defaults)
|
53
|
-
error_map(op._error_map)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
|
58
|
-
def default(pairs)
|
59
|
-
self._defaults.merge!(pairs.stringify_keys)
|
60
|
-
end
|
61
|
-
alias_method :defaults, :default
|
62
|
-
|
63
|
-
|
64
|
-
def error_map(map)
|
65
|
-
self._error_map.merge!(map)
|
66
|
-
end
|
67
|
-
alias_method :error_maps, :error_map
|
68
|
-
|
69
|
-
|
70
|
-
def inherited(child)
|
71
|
-
super
|
72
|
-
|
73
|
-
child._fields = []
|
74
|
-
child._defaults = {}
|
75
|
-
child._error_map = {}
|
76
|
-
|
77
|
-
child._fields |= self._fields
|
78
|
-
child._defaults.merge!(self._defaults)
|
79
|
-
child._error_map.merge!(self._error_map)
|
80
|
-
end
|
81
|
-
|
82
|
-
|
83
|
-
def submit!(*args)
|
84
|
-
op = new(*args)
|
85
|
-
op.submit!
|
86
|
-
|
87
|
-
op
|
88
|
-
end
|
89
|
-
|
90
|
-
def submit(*args)
|
91
|
-
op = new(*args)
|
92
|
-
op.submit
|
93
|
-
op
|
94
|
-
end
|
95
|
-
|
96
|
-
protected
|
97
|
-
|
98
|
-
def _field(field_name, options = {})
|
99
|
-
field = [options[:scope], field_name].compact.join('_')
|
100
|
-
self._fields += [field]
|
101
|
-
|
102
|
-
attr_accessor field
|
103
|
-
|
104
|
-
default(field => options[:default]) if options[:default]
|
105
|
-
end
|
106
|
-
|
107
|
-
end
|
108
|
-
|
109
|
-
|
110
|
-
class_attribute :_fields
|
111
|
-
self._fields = []
|
112
|
-
class_attribute :_defaults
|
113
|
-
self._defaults = {}
|
114
|
-
class_attribute :_error_map
|
115
|
-
self._error_map = {}
|
116
|
-
|
117
|
-
attr_reader :original_params
|
118
|
-
attr_reader :params
|
119
|
-
|
120
|
-
|
121
|
-
def initialize(inputs = {})
|
122
|
-
@original_params = inputs.with_indifferent_access
|
123
|
-
@params = {}
|
124
|
-
|
125
|
-
self.class._defaults.each do |k,v|
|
126
|
-
self.send("#{k}=", v.respond_to?(:call) ? v.call : v)
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
|
131
|
-
def submit!
|
132
|
-
unless submit
|
133
|
-
raise ::Subroutine::Failure.new(self)
|
134
|
-
end
|
135
|
-
true
|
136
|
-
end
|
137
|
-
|
138
|
-
# the action which should be invoked upon form submission (from the controller)
|
139
|
-
def submit
|
140
|
-
observe_submission do
|
141
|
-
@params = filter_params(@original_params)
|
142
|
-
|
143
|
-
set_accessors(@params)
|
144
|
-
|
145
|
-
validate_and_perform
|
146
|
-
end
|
147
|
-
|
148
|
-
rescue Exception => e
|
149
|
-
if e.respond_to?(:record)
|
150
|
-
inherit_errors_from(e.record) unless e.record == self
|
151
|
-
false
|
152
|
-
else
|
153
|
-
raise e
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
protected
|
158
|
-
|
159
|
-
# these enable you to 1) add log output or 2) add performance monitoring such as skylight.
|
160
|
-
def observe_submission
|
161
|
-
yield
|
162
|
-
end
|
163
|
-
|
164
|
-
def observe_validation
|
165
|
-
yield
|
166
|
-
end
|
167
|
-
|
168
|
-
def observe_perform
|
169
|
-
yield
|
170
|
-
end
|
171
|
-
|
172
|
-
|
173
|
-
def validate_and_perform
|
174
|
-
bool = observe_validation do
|
175
|
-
valid?
|
176
|
-
end
|
177
|
-
return false unless bool
|
178
|
-
|
179
|
-
observe_perform do
|
180
|
-
perform
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
# implement this in your concrete class.
|
185
|
-
def perform
|
186
|
-
raise NotImplementedError
|
187
|
-
end
|
188
|
-
|
189
|
-
def field_provided?(key)
|
190
|
-
@params.has_key?(key)
|
191
|
-
end
|
192
|
-
|
193
|
-
|
194
|
-
# applies the errors to the form object from the child object, optionally at the namespace provided
|
195
|
-
def inherit_errors_from(object, namespace = nil)
|
196
|
-
inherit_errors(object.errors, namespace)
|
197
|
-
end
|
198
|
-
|
199
|
-
|
200
|
-
# applies the errors in error_object to self, optionally at the namespace provided
|
201
|
-
# returns false so failure cases can end with this invocation
|
202
|
-
def inherit_errors(error_object, namespace = nil)
|
203
|
-
error_object.each do |k,v|
|
204
|
-
|
205
|
-
keys = [k, [namespace, k].compact.join('_')].map(&:to_sym).uniq
|
206
|
-
keys = keys.map{|key| _error_map[key] || key }
|
207
|
-
|
208
|
-
match = keys.detect{|key| self.respond_to?(key) || @original_params.try(:has_key?, key) }
|
209
|
-
|
210
|
-
if match
|
211
|
-
errors.add(match, v)
|
212
|
-
else
|
213
|
-
errors.add(:base, error_object.full_message(k, v))
|
214
|
-
end
|
215
|
-
|
216
|
-
end
|
217
|
-
|
218
|
-
false
|
219
|
-
end
|
220
|
-
|
221
|
-
|
222
|
-
# if you want to use strong parameters or something in your form object you can do so here.
|
223
|
-
def filter_params(inputs)
|
224
|
-
inputs.slice(*_fields)
|
225
|
-
end
|
226
|
-
|
227
|
-
|
228
|
-
def set_accessors(inputs, namespace = nil)
|
229
|
-
inputs.each do |key, value|
|
230
|
-
|
231
|
-
setter = [namespace, key].compact.join('_')
|
232
|
-
|
233
|
-
if respond_to?("#{setter}=") && _fields.include?(setter)
|
234
|
-
send("#{setter}=", value)
|
235
|
-
elsif value.is_a?(Hash)
|
236
|
-
set_accessors(value, setter)
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
end
|
242
|
-
|
243
|
-
end
|
2
|
+
require "subroutine/op"
|
@@ -5,12 +5,12 @@ module Subroutine
|
|
5
5
|
|
6
6
|
def test_simple_fields_definition
|
7
7
|
op = ::SignupOp.new
|
8
|
-
assert_equal [
|
8
|
+
assert_equal [:email, :password], op._fields.keys.sort
|
9
9
|
end
|
10
10
|
|
11
11
|
def test_inherited_fields
|
12
12
|
op = ::AdminSignupOp.new
|
13
|
-
assert_equal [
|
13
|
+
assert_equal [:email, :password, :priveleges], op._fields.keys.sort
|
14
14
|
end
|
15
15
|
|
16
16
|
def test_class_attribute_usage
|
@@ -20,45 +20,28 @@ module Subroutine
|
|
20
20
|
bid = ::AdminSignupOp._fields.object_id
|
21
21
|
|
22
22
|
refute_equal sid, bid
|
23
|
-
|
24
|
-
sid = ::SignupOp._defaults.object_id
|
25
|
-
bid = ::AdminSignupOp._defaults.object_id
|
26
|
-
|
27
|
-
refute_equal sid, bid
|
28
|
-
|
29
|
-
sid = ::SignupOp._error_map.object_id
|
30
|
-
bid = ::AdminSignupOp._error_map.object_id
|
31
|
-
|
32
|
-
refute_equal sid, bid
|
33
|
-
|
34
23
|
end
|
35
24
|
|
36
25
|
def test_inputs_from_inherited_fields_without_inheriting_from_the_class
|
37
26
|
refute ::BusinessSignupOp < ::SignupOp
|
38
27
|
|
39
|
-
::SignupOp._fields.
|
40
|
-
|
41
|
-
end
|
28
|
+
user_fields = ::SignupOp._fields.keys
|
29
|
+
biz_fields = ::BusinessSignupOp._fields.keys
|
42
30
|
|
43
|
-
|
44
|
-
|
45
|
-
end
|
46
|
-
|
47
|
-
::SignupOp._error_map.each_pair do |k,v|
|
48
|
-
assert_equal v, ::BusinessSignupOp._error_map[k]
|
31
|
+
user_fields.each do |field|
|
32
|
+
assert_includes biz_fields, field
|
49
33
|
end
|
50
34
|
end
|
51
35
|
|
52
36
|
def test_defaults_declaration_options
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
'bar' => 'bar'
|
57
|
-
}
|
37
|
+
op = ::DefaultsOp.new
|
38
|
+
assert_equal 'foo', op.foo
|
39
|
+
assert_equal 'bar', op.bar
|
58
40
|
end
|
59
41
|
|
60
42
|
def test_inherited_defaults_override_correctly
|
61
|
-
|
43
|
+
op = ::InheritedDefaultsOp.new
|
44
|
+
assert_equal 'barstool', op.bar
|
62
45
|
end
|
63
46
|
|
64
47
|
def test_accessors_are_created
|
@@ -87,6 +70,9 @@ module Subroutine
|
|
87
70
|
assert_nil op.email
|
88
71
|
assert_nil op.password
|
89
72
|
assert_equal 'min', op.priveleges
|
73
|
+
|
74
|
+
op.priveleges = 'max'
|
75
|
+
assert_equal 'max', op.priveleges
|
90
76
|
end
|
91
77
|
|
92
78
|
def test_validations_are_evaluated_before_perform_is_invoked
|
@@ -107,7 +93,7 @@ module Subroutine
|
|
107
93
|
assert op.perform_called
|
108
94
|
refute op.perform_finished
|
109
95
|
|
110
|
-
assert_equal ["has gotta be @admin.com"], op.errors[:
|
96
|
+
assert_equal ["Email address has gotta be @admin.com"], op.errors[:base]
|
111
97
|
end
|
112
98
|
|
113
99
|
def test_when_valid_perform_completes_it_returns_control
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Subroutine
|
4
|
+
class TypeCasterTest < TestCase
|
5
|
+
|
6
|
+
def op
|
7
|
+
@op ||= TypeCastOp.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_integer_inputs
|
11
|
+
op.integer_input = nil
|
12
|
+
assert_equal nil, op.integer_input
|
13
|
+
|
14
|
+
op.integer_input = 'foo'
|
15
|
+
assert_equal 0, op.integer_input
|
16
|
+
|
17
|
+
op.integer_input = '4.5'
|
18
|
+
assert_equal 4, op.integer_input
|
19
|
+
|
20
|
+
op.integer_input = 0.5
|
21
|
+
assert_equal 0, op.integer_input
|
22
|
+
|
23
|
+
op.integer_input = 5.2
|
24
|
+
assert_equal 5, op.integer_input
|
25
|
+
|
26
|
+
op.integer_input = 6
|
27
|
+
assert_equal 6, op.integer_input
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_number_inputs
|
31
|
+
op.number_input = nil
|
32
|
+
assert_equal nil, op.number_input
|
33
|
+
|
34
|
+
op.number_input = 4
|
35
|
+
assert_equal 4.0, op.number_input
|
36
|
+
|
37
|
+
op.number_input = 0.5
|
38
|
+
assert_equal 0.5, op.number_input
|
39
|
+
|
40
|
+
op.number_input = 'foo'
|
41
|
+
assert_equal 0.0, op.number_input
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_string_inputs
|
45
|
+
op.string_input = nil
|
46
|
+
assert_equal nil, op.string_input
|
47
|
+
|
48
|
+
op.string_input = ""
|
49
|
+
assert_equal '', op.string_input
|
50
|
+
|
51
|
+
op.string_input = "foo"
|
52
|
+
assert_equal 'foo', op.string_input
|
53
|
+
|
54
|
+
op.string_input = 4
|
55
|
+
assert_equal '4', op.string_input
|
56
|
+
|
57
|
+
op.string_input = 4.2
|
58
|
+
assert_equal '4.2', op.string_input
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_boolean_inputs
|
62
|
+
op.boolean_input = nil
|
63
|
+
assert_equal nil, op.boolean_input
|
64
|
+
|
65
|
+
op.boolean_input = 'yes'
|
66
|
+
assert_equal true, op.boolean_input
|
67
|
+
|
68
|
+
op.boolean_input = 'no'
|
69
|
+
assert_equal false, op.boolean_input
|
70
|
+
|
71
|
+
op.boolean_input = 'true'
|
72
|
+
assert_equal true, op.boolean_input
|
73
|
+
|
74
|
+
op.boolean_input = 'false'
|
75
|
+
assert_equal false, op.boolean_input
|
76
|
+
|
77
|
+
op.boolean_input = 'ok'
|
78
|
+
assert_equal true, op.boolean_input
|
79
|
+
|
80
|
+
op.boolean_input = ''
|
81
|
+
assert_equal false, op.boolean_input
|
82
|
+
|
83
|
+
op.boolean_input = true
|
84
|
+
assert_equal true, op.boolean_input
|
85
|
+
|
86
|
+
op.boolean_input = false
|
87
|
+
assert_equal false, op.boolean_input
|
88
|
+
|
89
|
+
op.boolean_input = '1'
|
90
|
+
assert_equal true, op.boolean_input
|
91
|
+
|
92
|
+
op.boolean_input = '0'
|
93
|
+
assert_equal false, op.boolean_input
|
94
|
+
|
95
|
+
op.boolean_input = 1
|
96
|
+
assert_equal true, op.boolean_input
|
97
|
+
|
98
|
+
op.boolean_input = 0
|
99
|
+
assert_equal false, op.boolean_input
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_hash_inputs
|
103
|
+
op.object_input = nil
|
104
|
+
assert_equal nil, op.object_input
|
105
|
+
|
106
|
+
op.object_input = ''
|
107
|
+
assert_equal({}, op.object_input)
|
108
|
+
|
109
|
+
op.object_input = [[:a,:b]]
|
110
|
+
assert_equal({"a" => :b}, op.object_input)
|
111
|
+
|
112
|
+
op.object_input = false
|
113
|
+
assert_equal({}, op.object_input)
|
114
|
+
|
115
|
+
op.object_input = {foo: 'bar'}
|
116
|
+
assert_equal({'foo' => 'bar'}, op.object_input)
|
117
|
+
|
118
|
+
op.object_input = {"foo" => {:bar => :baz}}
|
119
|
+
assert_equal({"foo" => {:bar => :baz}}, op.object_input)
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_array_inputs
|
123
|
+
op.array_input = nil
|
124
|
+
assert_equal nil, op.array_input
|
125
|
+
|
126
|
+
op.array_input = ''
|
127
|
+
assert_equal [], op.array_input
|
128
|
+
|
129
|
+
op.array_input = 'foo'
|
130
|
+
assert_equal ['foo'], op.array_input
|
131
|
+
|
132
|
+
op.array_input = ['foo']
|
133
|
+
assert_equal ['foo'], op.array_input
|
134
|
+
|
135
|
+
op.array_input = {:bar => true}
|
136
|
+
assert_equal [{:bar => true}], op.array_input
|
137
|
+
end
|
138
|
+
|
139
|
+
def test_date_inputs
|
140
|
+
op.date_input = nil
|
141
|
+
assert_equal nil, op.date_input
|
142
|
+
|
143
|
+
op.date_input = "2022-12-22"
|
144
|
+
assert_equal ::Date, op.date_input.class
|
145
|
+
refute_equal ::DateTime, op.date_input.class
|
146
|
+
|
147
|
+
assert_equal 2022, op.date_input.year
|
148
|
+
assert_equal 12, op.date_input.month
|
149
|
+
assert_equal 22, op.date_input.day
|
150
|
+
|
151
|
+
op.date_input = "2023-05-05T10:00:30"
|
152
|
+
assert_equal ::Date, op.date_input.class
|
153
|
+
refute_equal ::DateTime, op.date_input.class
|
154
|
+
|
155
|
+
assert_equal 2023, op.date_input.year
|
156
|
+
assert_equal 5, op.date_input.month
|
157
|
+
assert_equal 5, op.date_input.day
|
158
|
+
|
159
|
+
op.date_input = "2020-05-03 13:44:45 -0400"
|
160
|
+
|
161
|
+
assert_equal ::Date, op.date_input.class
|
162
|
+
refute_equal ::DateTime, op.date_input.class
|
163
|
+
|
164
|
+
assert_equal 2020, op.date_input.year
|
165
|
+
assert_equal 5, op.date_input.month
|
166
|
+
assert_equal 3, op.date_input.day
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
def test_time_inputs
|
171
|
+
op.time_input = nil
|
172
|
+
assert_equal nil, op.time_input
|
173
|
+
|
174
|
+
op.time_input = "2022-12-22"
|
175
|
+
assert_equal ::Time, op.time_input.class
|
176
|
+
refute_equal ::DateTime, op.time_input.class
|
177
|
+
|
178
|
+
assert_equal 2022, op.time_input.year
|
179
|
+
assert_equal 12, op.time_input.month
|
180
|
+
assert_equal 22, op.time_input.day
|
181
|
+
assert_equal 0, op.time_input.hour
|
182
|
+
assert_equal 0, op.time_input.min
|
183
|
+
assert_equal 0, op.time_input.sec
|
184
|
+
|
185
|
+
op.time_input = "2023-05-05T10:00:30Z"
|
186
|
+
assert_equal ::Time, op.time_input.class
|
187
|
+
refute_equal ::DateTime, op.time_input.class
|
188
|
+
|
189
|
+
assert_equal 2023, op.time_input.year
|
190
|
+
assert_equal 5, op.time_input.month
|
191
|
+
assert_equal 5, op.time_input.day
|
192
|
+
assert_equal 10, op.time_input.hour
|
193
|
+
assert_equal 0, op.time_input.min
|
194
|
+
assert_equal 30, op.time_input.sec
|
195
|
+
end
|
196
|
+
|
197
|
+
def test_iso_date_inputs
|
198
|
+
op.iso_date_input = nil
|
199
|
+
assert_equal nil, op.iso_date_input
|
200
|
+
|
201
|
+
op.iso_date_input = "2022-12-22"
|
202
|
+
assert_equal ::String, op.iso_date_input.class
|
203
|
+
assert_equal "2022-12-22", op.iso_date_input
|
204
|
+
|
205
|
+
op.iso_date_input = Date.parse("2022-12-22")
|
206
|
+
assert_equal ::String, op.iso_date_input.class
|
207
|
+
assert_equal "2022-12-22", op.iso_date_input
|
208
|
+
end
|
209
|
+
|
210
|
+
def test_iso_time_inputs
|
211
|
+
op.iso_time_input = nil
|
212
|
+
assert_equal nil, op.iso_time_input
|
213
|
+
|
214
|
+
op.iso_time_input = "2022-12-22T10:30:24Z"
|
215
|
+
assert_equal ::String, op.iso_time_input.class
|
216
|
+
assert_equal "2022-12-22T10:30:24Z", op.iso_time_input
|
217
|
+
|
218
|
+
op.iso_time_input = Time.parse("2022-12-22T10:30:24Z")
|
219
|
+
assert_equal ::String, op.iso_time_input.class
|
220
|
+
assert_equal "2022-12-22T10:30:24Z", op.iso_time_input
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
end
|
data/test/support/ops.rb
CHANGED
@@ -18,14 +18,12 @@ end
|
|
18
18
|
|
19
19
|
class SignupOp < ::Subroutine::Op
|
20
20
|
|
21
|
-
|
22
|
-
|
21
|
+
string :email
|
22
|
+
string :password
|
23
23
|
|
24
24
|
validates :email, :presence => true
|
25
25
|
validates :password, :presence => true
|
26
26
|
|
27
|
-
error_map :email_address => :email
|
28
|
-
|
29
27
|
attr_reader :perform_called
|
30
28
|
attr_reader :perform_finished
|
31
29
|
|
@@ -74,7 +72,7 @@ end
|
|
74
72
|
|
75
73
|
class BusinessSignupOp < ::Subroutine::Op
|
76
74
|
|
77
|
-
|
75
|
+
string :business_name
|
78
76
|
inputs_from ::SignupOp
|
79
77
|
|
80
78
|
end
|
@@ -82,16 +80,27 @@ end
|
|
82
80
|
class DefaultsOp < ::Subroutine::Op
|
83
81
|
|
84
82
|
field :foo, :default => 'foo'
|
85
|
-
|
86
|
-
field baz: 'baz'
|
87
|
-
|
88
|
-
field :bar
|
89
|
-
default :bar => 'bar'
|
83
|
+
field :bar, :default => 'bar'
|
90
84
|
|
91
85
|
end
|
92
86
|
|
93
87
|
class InheritedDefaultsOp < ::DefaultsOp
|
94
88
|
|
95
|
-
|
89
|
+
field :bar, :default => 'barstool'
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
class TypeCastOp < ::Subroutine::Op
|
94
|
+
|
95
|
+
integer :integer_input
|
96
|
+
number :number_input
|
97
|
+
string :string_input
|
98
|
+
boolean :boolean_input
|
99
|
+
date :date_input
|
100
|
+
time :time_input, :default => lambda{ Time.now }
|
101
|
+
iso_date :iso_date_input
|
102
|
+
iso_time :iso_time_input
|
103
|
+
object :object_input
|
104
|
+
array :array_input, :default => 'foo'
|
96
105
|
|
97
106
|
end
|
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: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Nelson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-05-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -100,9 +100,13 @@ files:
|
|
100
100
|
- gemfiles/am41.gemfile
|
101
101
|
- gemfiles/am42.gemfile
|
102
102
|
- lib/subroutine.rb
|
103
|
+
- lib/subroutine/failure.rb
|
104
|
+
- lib/subroutine/op.rb
|
105
|
+
- lib/subroutine/type_caster.rb
|
103
106
|
- lib/subroutine/version.rb
|
104
107
|
- subroutine.gemspec
|
105
108
|
- test/subroutine/base_test.rb
|
109
|
+
- test/subroutine/type_caster_test.rb
|
106
110
|
- test/support/ops.rb
|
107
111
|
- test/test_helper.rb
|
108
112
|
homepage: https://github.com/mnelson/subroutine
|
@@ -137,5 +141,6 @@ test_files:
|
|
137
141
|
- gemfiles/am41.gemfile
|
138
142
|
- gemfiles/am42.gemfile
|
139
143
|
- test/subroutine/base_test.rb
|
144
|
+
- test/subroutine/type_caster_test.rb
|
140
145
|
- test/support/ops.rb
|
141
146
|
- test/test_helper.rb
|