command_model 1.3.0 → 2.0.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/.travis.yml +0 -2
- data/README.md +12 -6
- data/command_model.gemspec +4 -6
- data/lib/command_model.rb +1 -0
- data/lib/command_model/convert.rb +85 -0
- data/lib/command_model/model.rb +52 -49
- data/lib/command_model/version.rb +1 -1
- data/spec/convert_spec.rb +139 -0
- data/spec/model_spec.rb +61 -124
- metadata +13 -54
- data/Guardfile +0 -9
- data/gemfiles/4.2.gemfile +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e62322d3750507b2218d0e2662517d807c4207a5f225ee98ab9eda6d39a9206
|
4
|
+
data.tar.gz: 5350099e5f9e77a68441faf7fab88fdcc736c36a0d1ac9dbd9d48ca75785cf14
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2aede9435d4d3a0dc779101807f02def57e15764b352d2228a89aa91fc341c098fcf17919f85ec2a9e222a97535b0c9e2a540dc2a104de72262d3f875e1b65c0
|
7
|
+
data.tar.gz: 7a01b95bf05e04b86c4735d21b1dae876e34dc4de3f1e6117c166dbeb46c191d7f05df10889791ad60be0355134edd2866598c02d80962984afdeab8891056b9
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
# CommandModel
|
4
4
|
|
5
5
|
CommandModel is an ActiveModel based class that encapsulates the user
|
6
|
-
interaction logic that wraps a domain operation. This user interaction
|
7
|
-
|
8
|
-
|
6
|
+
interaction logic that wraps a domain operation. This user interaction typically
|
7
|
+
may include sanitizing, validating, normalizing, and type converting input. It
|
8
|
+
also will include the response from the domain operation.
|
9
9
|
|
10
10
|
There are three major concerns when handling a user request: input handling,
|
11
11
|
domain logic, and persistence. ActiveRecord mixes all three of these concerns
|
@@ -21,7 +21,7 @@ ActiveRecord style update_attributes.
|
|
21
21
|
account.withdraw amount: 50
|
22
22
|
|
23
23
|
But there are multiple complications with the OO approach. How do we integrate
|
24
|
-
Rails style validations? How are user-supplied strings
|
24
|
+
Rails style validations? How are user-supplied strings type converted? How do we
|
25
25
|
know if the command succeeded? CommandModel solves these problems.
|
26
26
|
|
27
27
|
## Installation
|
@@ -45,7 +45,7 @@ request.
|
|
45
45
|
|
46
46
|
class WithdrawCommand < CommandModel::Model
|
47
47
|
parameter :amount,
|
48
|
-
|
48
|
+
convert: :integer,
|
49
49
|
presence: true,
|
50
50
|
numericality: { greater_than: 0, less_than_or_equal_to: 500 }
|
51
51
|
end
|
@@ -95,7 +95,7 @@ internal domain logic.
|
|
95
95
|
|
96
96
|
class WithdrawCommand < CommandModel::Model
|
97
97
|
parameter :amount,
|
98
|
-
|
98
|
+
convert: :integer,
|
99
99
|
presence: true,
|
100
100
|
numericality: { greater_than: 0, less_than_or_equal_to: 500 }
|
101
101
|
parameter :account_id, presence: true
|
@@ -130,6 +130,12 @@ integration of Rails form helpers and validations with CommandModel.
|
|
130
130
|
|
131
131
|
## Version History
|
132
132
|
|
133
|
+
* 2.0 - April 11, 2018
|
134
|
+
* Rename typecast parameter option to convert
|
135
|
+
* Any callable object can be used as a type converter
|
136
|
+
* Multiple type converters can be chained together
|
137
|
+
* Added StringMutator type converter
|
138
|
+
* Add boolean type conversion
|
133
139
|
* 1.3 - February 13, 2018
|
134
140
|
* Add decimal type cast
|
135
141
|
* 1.2 - October 24, 2014
|
data/command_model.gemspec
CHANGED
@@ -15,12 +15,10 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = CommandModel::VERSION
|
17
17
|
|
18
|
-
gem.
|
18
|
+
gem.required_ruby_version = '>= 2.5.0'
|
19
19
|
|
20
|
-
gem.
|
21
|
-
gem.add_development_dependency 'rspec', "~> 2.14.1"
|
22
|
-
gem.add_development_dependency 'guard', "~> 2.14.2"
|
23
|
-
gem.add_development_dependency 'guard-rspec', "~> 3.1.0"
|
24
|
-
gem.add_development_dependency 'rb-fsevent', '~> 0.10.2'
|
20
|
+
gem.add_dependency 'activemodel', "> 5.0"
|
25
21
|
|
22
|
+
gem.add_development_dependency 'rake', "~> 12.3.0"
|
23
|
+
gem.add_development_dependency 'rspec', "~> 3.7.0"
|
26
24
|
end
|
data/lib/command_model.rb
CHANGED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module CommandModel
|
5
|
+
module Convert
|
6
|
+
class ConvertError < StandardError
|
7
|
+
attr_reader :original_error, :target_type
|
8
|
+
|
9
|
+
def initialize(original_error, target_type)
|
10
|
+
@original_error = original_error
|
11
|
+
@target_type = target_type
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class StringMutator
|
16
|
+
def initialize(force_to_s=false, &block)
|
17
|
+
@force_to_s = force_to_s
|
18
|
+
@mutator = block
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(value)
|
22
|
+
if @force_to_s
|
23
|
+
@mutator.call value.to_s
|
24
|
+
elsif value.respond_to? :to_str
|
25
|
+
@mutator.call value.to_str
|
26
|
+
else
|
27
|
+
value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Integer
|
33
|
+
def call(value)
|
34
|
+
return nil if value.blank?
|
35
|
+
Integer(value)
|
36
|
+
rescue StandardError => e
|
37
|
+
raise ConvertError.new(e, "integer")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Decimal
|
42
|
+
def call(value)
|
43
|
+
return nil if value.blank?
|
44
|
+
BigDecimal(value, 16)
|
45
|
+
rescue StandardError => e
|
46
|
+
raise ConvertError.new(e, "number")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Float
|
51
|
+
def call(value)
|
52
|
+
return nil if value.blank?
|
53
|
+
Float(value)
|
54
|
+
rescue StandardError => e
|
55
|
+
raise ConvertError.new(e, "number")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Date
|
60
|
+
def call(value)
|
61
|
+
return nil if value.blank?
|
62
|
+
return value if value.kind_of? Date
|
63
|
+
value = value.to_s
|
64
|
+
if value =~ /\A(\d\d\d\d)-(\d\d)-(\d\d)\z/
|
65
|
+
::Date.civil($1.to_i, $2.to_i, $3.to_i)
|
66
|
+
else
|
67
|
+
::Date.strptime(value, "%m/%d/%Y")
|
68
|
+
end
|
69
|
+
rescue StandardError => e
|
70
|
+
raise ConvertError.new(e, "date")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Boolean
|
75
|
+
def call(value)
|
76
|
+
case value
|
77
|
+
when "", "0", "false", "f", 0
|
78
|
+
then false
|
79
|
+
else
|
80
|
+
!!value
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/command_model/model.rb
CHANGED
@@ -1,45 +1,54 @@
|
|
1
1
|
module CommandModel
|
2
|
+
class TypecastError < StandardError
|
3
|
+
attr_reader :original_error
|
4
|
+
|
5
|
+
def initialize(original_error)
|
6
|
+
@original_error = original_error
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
2
10
|
class Model
|
3
11
|
include ActiveModel::Validations
|
4
12
|
include ActiveModel::Conversion
|
5
13
|
extend ActiveModel::Naming
|
6
14
|
|
7
|
-
Parameter = Struct.new(:name, :
|
15
|
+
Parameter = Struct.new(:name, :converters, :validations)
|
8
16
|
|
9
17
|
# Parameter requires one or more attributes as its first parameter(s).
|
10
18
|
# It accepts an options hash as its last parameter.
|
11
19
|
#
|
12
20
|
# ==== Options
|
13
21
|
#
|
14
|
-
# *
|
15
|
-
#
|
16
|
-
#
|
22
|
+
# * convert - An object or array of objects that respond to call and
|
23
|
+
# convert the assigned value as necessary. Built-in converters exist
|
24
|
+
# for integer, decimal, float, date, and boolean. These built-in
|
25
|
+
# converters can be specified by symbol.
|
17
26
|
# * validations - All other options are considered validations and are
|
18
27
|
# passed to ActiveModel::Validates.validates
|
19
28
|
#
|
20
29
|
# ==== Examples
|
21
30
|
#
|
22
31
|
# parameter :gender
|
23
|
-
# parameter :name, :
|
24
|
-
# parameter :birthdate, :
|
32
|
+
# parameter :name, presence: true
|
33
|
+
# parameter :birthdate, convert: :date
|
25
34
|
# parameter :height, :weight,
|
26
|
-
# :
|
27
|
-
# :
|
28
|
-
# :
|
35
|
+
# convert: [CommandModel::Convert::StringMutator.new { |s| s.gsub(",", "")}, :integer],
|
36
|
+
# presence: true,
|
37
|
+
# numericality: { :greater_than_or_equal_to => 0 }
|
29
38
|
def self.parameter(*args)
|
30
39
|
options = args.last.kind_of?(Hash) ? args.pop.clone : {}
|
31
|
-
|
40
|
+
converters = options.delete(:convert)
|
32
41
|
|
33
42
|
args.each do |name|
|
34
43
|
attr_reader name
|
35
44
|
|
36
|
-
if
|
37
|
-
|
45
|
+
if converters
|
46
|
+
attr_type_converting_writer name, Array(converters)
|
38
47
|
else
|
39
48
|
attr_writer name
|
40
49
|
end
|
41
50
|
validates name, options.clone if options.present? # clone options because validates mutates the hash :(
|
42
|
-
parameters.push Parameter.new name,
|
51
|
+
parameters.push Parameter.new name, converters, options
|
43
52
|
end
|
44
53
|
end
|
45
54
|
|
@@ -48,21 +57,37 @@ module CommandModel
|
|
48
57
|
@parameters ||= []
|
49
58
|
end
|
50
59
|
|
51
|
-
def self.
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
60
|
+
def self.attr_type_converting_writer(name, converters) #:nodoc
|
61
|
+
converters = converters.map do |c|
|
62
|
+
if c.respond_to? :call
|
63
|
+
c
|
64
|
+
else
|
65
|
+
case c.to_s
|
66
|
+
when "integer"
|
67
|
+
CommandModel::Convert::Integer.new
|
68
|
+
when "decimal"
|
69
|
+
CommandModel::Convert::Decimal.new
|
70
|
+
when "float"
|
71
|
+
CommandModel::Convert::Float.new
|
72
|
+
when "date"
|
73
|
+
CommandModel::Convert::Date.new
|
74
|
+
when "boolean"
|
75
|
+
CommandModel::Convert::Boolean.new
|
58
76
|
else
|
59
|
-
|
60
|
-
@#{name} = value
|
77
|
+
raise ArgumentError, "unknown converter #{c}"
|
61
78
|
end
|
62
|
-
|
63
|
-
@#{name}
|
64
79
|
end
|
65
|
-
|
80
|
+
end
|
81
|
+
|
82
|
+
define_method "#{name}=" do |value|
|
83
|
+
converted_value = converters.reduce(value) { |v, c| c.call(v) }
|
84
|
+
instance_variable_set "@#{name}", converted_value
|
85
|
+
instance_variable_get("@type_conversion_errors").delete(name)
|
86
|
+
instance_variable_get "@#{name}"
|
87
|
+
rescue CommandModel::Convert::ConvertError => e
|
88
|
+
instance_variable_get("@type_conversion_errors")[name] = e.target_type
|
89
|
+
instance_variable_set "@#{name}", value
|
90
|
+
end
|
66
91
|
end
|
67
92
|
|
68
93
|
# Executes a block of code if the command model is valid.
|
@@ -113,7 +138,7 @@ module CommandModel
|
|
113
138
|
# instance of the same class is passed in then the parameters are copied
|
114
139
|
# to the new object.
|
115
140
|
def initialize(parameters={})
|
116
|
-
@
|
141
|
+
@type_conversion_errors = {}
|
117
142
|
set_parameters parameters
|
118
143
|
end
|
119
144
|
|
@@ -179,30 +204,8 @@ module CommandModel
|
|
179
204
|
end
|
180
205
|
end
|
181
206
|
|
182
|
-
def typecast_integer(value)
|
183
|
-
Integer(value) rescue nil
|
184
|
-
end
|
185
|
-
|
186
|
-
def typecast_decimal(value)
|
187
|
-
BigDecimal(value, 16) rescue nil
|
188
|
-
end
|
189
|
-
|
190
|
-
def typecast_float(value)
|
191
|
-
Float(value) rescue nil
|
192
|
-
end
|
193
|
-
|
194
|
-
def typecast_date(value)
|
195
|
-
return value if value.kind_of? Date
|
196
|
-
value = value.to_s
|
197
|
-
if value =~ /\A(\d\d\d\d)-(\d\d)-(\d\d)\z/
|
198
|
-
Date.civil($1.to_i, $2.to_i, $3.to_i) rescue nil
|
199
|
-
else
|
200
|
-
Date.strptime(value, "%m/%d/%Y") rescue nil
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
207
|
def include_typecasting_errors
|
205
|
-
@
|
208
|
+
@type_conversion_errors.each do |attribute, target_type|
|
206
209
|
unless errors[attribute].present?
|
207
210
|
errors.add attribute, "is not a #{target_type}"
|
208
211
|
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "CommandModel::Convert" do
|
4
|
+
describe "StringMutator" do
|
5
|
+
subject { CommandModel::Convert::StringMutator.new { |s| s.gsub(",", "") } }
|
6
|
+
|
7
|
+
it "strips commas from strings" do
|
8
|
+
expect(subject.("1,000")).to eq("1000")
|
9
|
+
end
|
10
|
+
|
11
|
+
it "does nothing to non-strings" do
|
12
|
+
expect(subject.(nil)).to eq(nil)
|
13
|
+
expect(subject.(42)).to eq(42)
|
14
|
+
expect(subject.(:bla)).to eq(:bla)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "Integer" do
|
19
|
+
subject { CommandModel::Convert::Integer.new }
|
20
|
+
|
21
|
+
it "casts to integer when valid string" do
|
22
|
+
expect(subject.("42")).to eq(42)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "accepts nil" do
|
26
|
+
expect(subject.(nil)).to eq(nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "converts empty string to nil" do
|
30
|
+
expect(subject.("")).to eq(nil)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "raises TypecastError when invalid string" do
|
34
|
+
expect { subject.("asdf") }.to raise_error(CommandModel::Convert::ConvertError)
|
35
|
+
expect { subject.("0.1") }.to raise_error(CommandModel::Convert::ConvertError)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "Float" do
|
40
|
+
subject { CommandModel::Convert::Float.new }
|
41
|
+
|
42
|
+
it "casts to float when valid string" do
|
43
|
+
expect(subject.("42")).to eq(42.0)
|
44
|
+
expect(subject.("42.5")).to eq(42.5)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "accepts nil" do
|
48
|
+
expect(subject.(nil)).to eq(nil)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "converts empty string to nil" do
|
52
|
+
expect(subject.("")).to eq(nil)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "raises TypecastError when invalid string" do
|
56
|
+
expect { subject.("asdf") }.to raise_error(CommandModel::Convert::ConvertError)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "Decimal" do
|
61
|
+
subject { CommandModel::Convert::Decimal.new }
|
62
|
+
|
63
|
+
it "converts to BigDecimal when valid string" do
|
64
|
+
expect(subject.("42")).to eq(BigDecimal("42"))
|
65
|
+
expect(subject.("42.5")).to eq(BigDecimal("42.5"))
|
66
|
+
end
|
67
|
+
|
68
|
+
it "converts to BigDecimal when float" do
|
69
|
+
expect(subject.(42.0)).to eq(BigDecimal("42"))
|
70
|
+
end
|
71
|
+
|
72
|
+
it "converts to BigDecimal when int" do
|
73
|
+
expect(subject.(42)).to eq(BigDecimal("42"))
|
74
|
+
end
|
75
|
+
|
76
|
+
it "accepts nil" do
|
77
|
+
expect(subject.(nil)).to eq(nil)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "converts empty string to nil" do
|
81
|
+
expect(subject.("")).to eq(nil)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "raises TypecastError when invalid string" do
|
85
|
+
expect { subject.("asdf") }.to raise_error(CommandModel::Convert::ConvertError)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "Date" do
|
90
|
+
subject { CommandModel::Convert::Date.new }
|
91
|
+
|
92
|
+
it "casts to date when valid string" do
|
93
|
+
expect(subject.("01/01/2000")).to eq(Date.civil(2000,1,1))
|
94
|
+
expect(subject.("1/1/2000")).to eq(Date.civil(2000,1,1))
|
95
|
+
expect(subject.("2000-01-01")).to eq(Date.civil(2000,1,1))
|
96
|
+
end
|
97
|
+
|
98
|
+
it "returns existing date unchanged" do
|
99
|
+
date = Date.civil(2000,1,1)
|
100
|
+
expect(subject.(date)).to eq(date)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "accepts nil" do
|
104
|
+
expect(subject.(nil)).to eq(nil)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "converts empty string to nil" do
|
108
|
+
expect(subject.("")).to eq(nil)
|
109
|
+
end
|
110
|
+
|
111
|
+
it "raises TypecastError when invalid string" do
|
112
|
+
expect { subject.("asdf") }.to raise_error(CommandModel::Convert::ConvertError)
|
113
|
+
expect { subject.("3/50/1290") }.to raise_error(CommandModel::Convert::ConvertError)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "Boolean" do
|
118
|
+
subject { CommandModel::Convert::Boolean.new }
|
119
|
+
|
120
|
+
it "casts to true when any non-false value" do
|
121
|
+
expect(subject.("true")).to eq(true)
|
122
|
+
expect(subject.("t")).to eq(true)
|
123
|
+
expect(subject.("1")).to eq(true)
|
124
|
+
expect(subject.(true)).to eq(true)
|
125
|
+
expect(subject.(Object.new)).to eq(true)
|
126
|
+
expect(subject.(42)).to eq(true)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "casts to false when false values" do
|
130
|
+
expect(subject.("")).to eq(false)
|
131
|
+
expect(subject.("0")).to eq(false)
|
132
|
+
expect(subject.("f")).to eq(false)
|
133
|
+
expect(subject.("false")).to eq(false)
|
134
|
+
expect(subject.(0)).to eq(false)
|
135
|
+
expect(subject.(nil)).to eq(false)
|
136
|
+
expect(subject.(false)).to eq(false)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/spec/model_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
class ExampleCommand < CommandModel::Model
|
4
|
-
parameter :name, :
|
4
|
+
parameter :name, presence: true
|
5
5
|
parameter :title
|
6
6
|
end
|
7
7
|
|
@@ -14,57 +14,63 @@ describe CommandModel::Model do
|
|
14
14
|
|
15
15
|
it "creates an attribute reader" do
|
16
16
|
klass.parameter :foo
|
17
|
-
klass.new.methods.
|
17
|
+
expect(klass.new.methods).to include(:foo)
|
18
18
|
end
|
19
19
|
|
20
20
|
it "creates an attribute writer" do
|
21
21
|
klass.parameter :foo
|
22
|
-
klass.new.methods.
|
22
|
+
expect(klass.new.methods).to include(:foo=)
|
23
23
|
end
|
24
24
|
|
25
25
|
it "round trips values through writing and reading" do
|
26
26
|
klass.parameter :foo
|
27
27
|
instance = klass.new
|
28
28
|
instance.foo = 42
|
29
|
-
instance.foo.
|
29
|
+
expect(instance.foo).to eq(42)
|
30
30
|
end
|
31
31
|
|
32
32
|
it "accepts multiple attributes" do
|
33
33
|
klass.parameter :foo, :bar
|
34
|
-
klass.new.methods.
|
35
|
-
klass.new.methods.
|
36
|
-
klass.new.methods.
|
37
|
-
klass.new.methods.
|
34
|
+
expect(klass.new.methods).to include(:foo)
|
35
|
+
expect(klass.new.methods).to include(:foo=)
|
36
|
+
expect(klass.new.methods).to include(:bar)
|
37
|
+
expect(klass.new.methods).to include(:bar=)
|
38
38
|
end
|
39
39
|
|
40
|
-
it "accepts multiple attributes with
|
41
|
-
klass.parameter :foo, :bar, :
|
42
|
-
klass.new.methods.
|
43
|
-
klass.new.methods.
|
44
|
-
klass.new.methods.
|
45
|
-
klass.new.methods.
|
40
|
+
it "accepts multiple attributes with convert" do
|
41
|
+
klass.parameter :foo, :bar, :convert => :integer
|
42
|
+
expect(klass.new.methods).to include(:foo)
|
43
|
+
expect(klass.new.methods).to include(:foo=)
|
44
|
+
expect(klass.new.methods).to include(:bar)
|
45
|
+
expect(klass.new.methods).to include(:bar=)
|
46
46
|
end
|
47
47
|
|
48
48
|
it "accepts multiple attributes with validation" do
|
49
49
|
klass.parameter :foo, :bar, :presence => true
|
50
|
-
klass.new.methods.
|
51
|
-
klass.new.methods.
|
52
|
-
klass.new.methods.
|
53
|
-
klass.new.methods.
|
50
|
+
expect(klass.new.methods).to include(:foo)
|
51
|
+
expect(klass.new.methods).to include(:foo=)
|
52
|
+
expect(klass.new.methods).to include(:bar)
|
53
|
+
expect(klass.new.methods).to include(:bar=)
|
54
54
|
end
|
55
55
|
|
56
|
-
it "
|
57
|
-
klass.
|
58
|
-
klass.parameter :answer, :typecast => "42"
|
56
|
+
it "converts via callable" do
|
57
|
+
klass.parameter :answer, convert: ->(value) { 42 }
|
59
58
|
instance = klass.new
|
60
59
|
instance.answer = "foo"
|
61
|
-
instance.answer.
|
60
|
+
expect(instance.answer).to eq(42)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "converts with multiple converters" do
|
64
|
+
klass.parameter :num, convert: [CommandModel::Convert::StringMutator.new { |s| s.gsub(",", "")}, :integer]
|
65
|
+
instance = klass.new
|
66
|
+
instance.num = "1,000"
|
67
|
+
expect(instance.num).to eq(1000)
|
62
68
|
end
|
63
69
|
|
64
70
|
it "creates validations" do
|
65
71
|
instance = ExampleCommand.new
|
66
|
-
instance.
|
67
|
-
instance.errors[:name].
|
72
|
+
expect(instance).to_not be_valid
|
73
|
+
expect(instance.errors[:name]).to be_present
|
68
74
|
end
|
69
75
|
end
|
70
76
|
|
@@ -72,7 +78,7 @@ describe CommandModel::Model do
|
|
72
78
|
it "returns all parameters in class" do
|
73
79
|
klass = Class.new(CommandModel::Model)
|
74
80
|
klass.parameter :name, presence: true
|
75
|
-
klass.parameter :birthdate,
|
81
|
+
klass.parameter :birthdate, convert: :date, presence: true
|
76
82
|
|
77
83
|
expected = [
|
78
84
|
CommandModel::Model::Parameter.new(:name, nil, { presence: true }),
|
@@ -85,35 +91,35 @@ describe CommandModel::Model do
|
|
85
91
|
|
86
92
|
describe "self.execute" do
|
87
93
|
it "accepts object of same kind and returns it" do
|
88
|
-
ExampleCommand.execute(example_command) {}.
|
94
|
+
expect(ExampleCommand.execute(example_command) {}).to eq(example_command)
|
89
95
|
end
|
90
96
|
|
91
97
|
it "accepts attributes, creates object, and returns it" do
|
92
98
|
c = ExampleCommand.execute(:name => "John") {}
|
93
|
-
c.
|
94
|
-
c.name.
|
99
|
+
expect(c).to be_kind_of(ExampleCommand)
|
100
|
+
expect(c.name).to eq("John")
|
95
101
|
end
|
96
102
|
|
97
103
|
it "calls passed block when there are no validation errors on Model" do
|
98
104
|
block_ran = false
|
99
105
|
ExampleCommand.execute(example_command) { block_ran = true }
|
100
|
-
block_ran.
|
106
|
+
expect(block_ran).to eq(true)
|
101
107
|
end
|
102
108
|
|
103
109
|
it "does not call passed block when there are validation errors on Model" do
|
104
110
|
block_ran = false
|
105
111
|
ExampleCommand.execute(invalid_example_command) { block_ran = true }
|
106
|
-
block_ran.
|
112
|
+
expect(block_ran).to eq(false)
|
107
113
|
end
|
108
114
|
|
109
115
|
it "records execution attempt when there not no validation errors on Model" do
|
110
116
|
ExampleCommand.execute(example_command) {}
|
111
|
-
example_command.execution_attempted
|
117
|
+
expect(example_command.execution_attempted?).to eq(true)
|
112
118
|
end
|
113
119
|
|
114
120
|
it "records execution attempt when there are validation errors on Model" do
|
115
121
|
ExampleCommand.execute(invalid_example_command) {}
|
116
|
-
invalid_example_command.execution_attempted
|
122
|
+
expect(invalid_example_command.execution_attempted?).to eq(true)
|
117
123
|
end
|
118
124
|
|
119
125
|
it "is not successful if block adds error to Model" do
|
@@ -121,24 +127,24 @@ describe CommandModel::Model do
|
|
121
127
|
command.errors.add :base, "foo"
|
122
128
|
end
|
123
129
|
|
124
|
-
example_command.
|
130
|
+
expect(example_command).to_not be_success
|
125
131
|
end
|
126
132
|
end
|
127
133
|
|
128
134
|
describe "self.success" do
|
129
135
|
it "creates a successful command model" do
|
130
136
|
response = ExampleCommand.success
|
131
|
-
response.
|
132
|
-
response.
|
137
|
+
expect(response).to be_kind_of(ExampleCommand)
|
138
|
+
expect(response).to be_success
|
133
139
|
end
|
134
140
|
end
|
135
141
|
|
136
142
|
describe "self.failure" do
|
137
143
|
it "creates a command model with an error" do
|
138
144
|
response = ExampleCommand.failure "something broke"
|
139
|
-
response.
|
140
|
-
response.
|
141
|
-
response.errors[:base].
|
145
|
+
expect(response).to be_kind_of(ExampleCommand)
|
146
|
+
expect(response).to_not be_success
|
147
|
+
expect(response.errors[:base]).to eq(["something broke"])
|
142
148
|
end
|
143
149
|
end
|
144
150
|
|
@@ -146,7 +152,7 @@ describe CommandModel::Model do
|
|
146
152
|
describe "initialize" do
|
147
153
|
it "assigns parameters from hash" do
|
148
154
|
m = ExampleCommand.new :name => "John"
|
149
|
-
m.name.
|
155
|
+
expect(m.name).to eq("John")
|
150
156
|
end
|
151
157
|
|
152
158
|
it "assigns parameters from other CommandModel" do
|
@@ -159,7 +165,7 @@ describe CommandModel::Model do
|
|
159
165
|
describe "call" do
|
160
166
|
context "when valid" do
|
161
167
|
it "calls execute" do
|
162
|
-
example_command.
|
168
|
+
expect(example_command).to receive(:execute)
|
163
169
|
example_command.call
|
164
170
|
end
|
165
171
|
|
@@ -170,7 +176,7 @@ describe CommandModel::Model do
|
|
170
176
|
|
171
177
|
context "when invalid" do
|
172
178
|
it "does not call execute" do
|
173
|
-
invalid_example_command.
|
179
|
+
expect(invalid_example_command).to_not receive(:execute)
|
174
180
|
invalid_example_command.call
|
175
181
|
end
|
176
182
|
|
@@ -194,24 +200,24 @@ describe CommandModel::Model do
|
|
194
200
|
describe "execution_attempted!" do
|
195
201
|
it "sets execution_attempted? to true" do
|
196
202
|
example_command.execution_attempted!
|
197
|
-
example_command.execution_attempted
|
203
|
+
expect(example_command.execution_attempted?).to eq(true)
|
198
204
|
end
|
199
205
|
end
|
200
206
|
|
201
207
|
describe "success?" do
|
202
208
|
it "is false before execution" do
|
203
|
-
example_command.
|
209
|
+
expect(example_command).to_not be_success
|
204
210
|
end
|
205
211
|
|
206
212
|
it "is false after execution with errors" do
|
207
213
|
example_command.execution_attempted!
|
208
214
|
example_command.errors.add :base, "foo"
|
209
|
-
example_command.success
|
215
|
+
expect(example_command.success?).to eq(false)
|
210
216
|
end
|
211
217
|
|
212
218
|
it "is true after execution without errors" do
|
213
219
|
example_command.execution_attempted!
|
214
|
-
example_command.success
|
220
|
+
expect(example_command.success?).to eq(true)
|
215
221
|
end
|
216
222
|
end
|
217
223
|
|
@@ -219,7 +225,7 @@ describe CommandModel::Model do
|
|
219
225
|
it "is a hash of all parameter name and values" do
|
220
226
|
klass = Class.new(CommandModel::Model)
|
221
227
|
klass.parameter :name, presence: true
|
222
|
-
klass.parameter :birthdate,
|
228
|
+
klass.parameter :birthdate, convert: :date, presence: true
|
223
229
|
|
224
230
|
expected = { name: "John", birthdate: Date.new(1980,1,1) }
|
225
231
|
instance = klass.new expected
|
@@ -241,85 +247,16 @@ describe CommandModel::Model do
|
|
241
247
|
end
|
242
248
|
end
|
243
249
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
it "returns nil when invalid string" do
|
250
|
-
example_command.send(:typecast_integer, "asdf").should be_nil
|
251
|
-
example_command.send(:typecast_integer, nil).should be_nil
|
252
|
-
example_command.send(:typecast_integer, "").should be_nil
|
253
|
-
example_command.send(:typecast_integer, "0.1").should be_nil
|
254
|
-
end
|
250
|
+
it "includes type conversion errors in validations" do
|
251
|
+
example_command.instance_variable_get(:@type_conversion_errors)["name"] = "integer"
|
252
|
+
expect(example_command).to_not be_valid
|
253
|
+
expect(example_command.errors["name"]).to be
|
255
254
|
end
|
256
255
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
it "returns nil when invalid string" do
|
264
|
-
example_command.send(:typecast_float, "asdf").should be_nil
|
265
|
-
example_command.send(:typecast_float, nil).should be_nil
|
266
|
-
example_command.send(:typecast_float, "").should be_nil
|
267
|
-
end
|
256
|
+
it "does not include type conversion error in validations if the attribute already has an error" do
|
257
|
+
invalid_example_command.instance_variable_get(:@type_conversion_errors)["name"] = "integer"
|
258
|
+
expect(invalid_example_command).to_not be_valid
|
259
|
+
expect(invalid_example_command.errors["name"]).to be
|
260
|
+
expect(invalid_example_command.errors["name"].find { |e| e =~ /integer/ }).to_not be
|
268
261
|
end
|
269
|
-
|
270
|
-
describe "typecast_decimal" do
|
271
|
-
it "converts to BigDecimal when valid string" do
|
272
|
-
example_command.send(:typecast_decimal, "42").should eq(BigDecimal("42"))
|
273
|
-
example_command.send(:typecast_decimal, "42.5").should eq(BigDecimal("42.5"))
|
274
|
-
end
|
275
|
-
|
276
|
-
it "converts to BigDecimal when float" do
|
277
|
-
example_command.send(:typecast_decimal, 42.0).should eq(BigDecimal("42"))
|
278
|
-
end
|
279
|
-
|
280
|
-
it "converts to BigDecimal when int" do
|
281
|
-
example_command.send(:typecast_decimal, 42).should eq(BigDecimal("42"))
|
282
|
-
end
|
283
|
-
|
284
|
-
it "returns nil when invalid string" do
|
285
|
-
example_command.send(:typecast_decimal, "asdf").should be_nil
|
286
|
-
example_command.send(:typecast_decimal, nil).should be_nil
|
287
|
-
example_command.send(:typecast_decimal, "").should be_nil
|
288
|
-
end
|
289
|
-
end
|
290
|
-
|
291
|
-
describe "typecast_date" do
|
292
|
-
it "casts to date when valid string" do
|
293
|
-
example_command.send(:typecast_date, "01/01/2000").should eq(Date.civil(2000,1,1))
|
294
|
-
example_command.send(:typecast_date, "1/1/2000").should eq(Date.civil(2000,1,1))
|
295
|
-
example_command.send(:typecast_date, "2000-01-01").should eq(Date.civil(2000,1,1))
|
296
|
-
end
|
297
|
-
|
298
|
-
it "returns existing date unchanged" do
|
299
|
-
date = Date.civil(2000,1,1)
|
300
|
-
example_command.send(:typecast_date, date).should eq(date)
|
301
|
-
end
|
302
|
-
|
303
|
-
it "returns nil when invalid string" do
|
304
|
-
example_command.send(:typecast_date, "asdf").should be_nil
|
305
|
-
example_command.send(:typecast_date, nil).should be_nil
|
306
|
-
example_command.send(:typecast_date, "").should be_nil
|
307
|
-
example_command.send(:typecast_date, "3/50/1290").should be_nil
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
it "includes typecasting errors in validations" do
|
312
|
-
example_command.instance_variable_get(:@typecast_errors)["name"] = "integer"
|
313
|
-
example_command.should_not be_valid
|
314
|
-
example_command.errors["name"].should be
|
315
|
-
end
|
316
|
-
|
317
|
-
it "does not include typecasting error in validations if the attribute already has an error" do
|
318
|
-
invalid_example_command.instance_variable_get(:@typecast_errors)["name"] = "integer"
|
319
|
-
invalid_example_command.should_not be_valid
|
320
|
-
invalid_example_command.errors["name"].should be
|
321
|
-
invalid_example_command.errors["name"].find { |e| e =~ /integer/ }.should_not be
|
322
|
-
end
|
323
|
-
|
324
|
-
|
325
262
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: command_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jack Christensen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-04-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -16,84 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '5.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 12.3.0
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 12.3.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 3.7.0
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: guard
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 2.14.2
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 2.14.2
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: guard-rspec
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - "~>"
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: 3.1.0
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - "~>"
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: 3.1.0
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: rb-fsevent
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - "~>"
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: 0.10.2
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - "~>"
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: 0.10.2
|
54
|
+
version: 3.7.0
|
97
55
|
description: CommandModel - when update_attributes isn't enough.
|
98
56
|
email:
|
99
57
|
- jack@jackchristensen.com
|
@@ -105,7 +63,6 @@ files:
|
|
105
63
|
- ".rspec"
|
106
64
|
- ".travis.yml"
|
107
65
|
- Gemfile
|
108
|
-
- Guardfile
|
109
66
|
- LICENSE
|
110
67
|
- README.md
|
111
68
|
- Rakefile
|
@@ -170,12 +127,13 @@ files:
|
|
170
127
|
- examples/bank/vendor/assets/javascripts/.gitkeep
|
171
128
|
- examples/bank/vendor/assets/stylesheets/.gitkeep
|
172
129
|
- examples/bank/vendor/plugins/.gitkeep
|
173
|
-
- gemfiles/4.2.gemfile
|
174
130
|
- gemfiles/5.0.gemfile
|
175
131
|
- gemfiles/5.1.gemfile
|
176
132
|
- lib/command_model.rb
|
133
|
+
- lib/command_model/convert.rb
|
177
134
|
- lib/command_model/model.rb
|
178
135
|
- lib/command_model/version.rb
|
136
|
+
- spec/convert_spec.rb
|
179
137
|
- spec/model_spec.rb
|
180
138
|
- spec/spec_helper.rb
|
181
139
|
homepage: https://github.com/JackC/command_model
|
@@ -189,7 +147,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
189
147
|
requirements:
|
190
148
|
- - ">="
|
191
149
|
- !ruby/object:Gem::Version
|
192
|
-
version:
|
150
|
+
version: 2.5.0
|
193
151
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
194
152
|
requirements:
|
195
153
|
- - ">="
|
@@ -197,12 +155,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
197
155
|
version: '0'
|
198
156
|
requirements: []
|
199
157
|
rubyforge_project:
|
200
|
-
rubygems_version: 2.7.
|
158
|
+
rubygems_version: 2.7.6
|
201
159
|
signing_key:
|
202
160
|
specification_version: 4
|
203
161
|
summary: CommandModel integrates Rails validations with command objects. This allows
|
204
162
|
errors from command execution to easily be handled with the familiar Rails validation
|
205
163
|
system.
|
206
164
|
test_files:
|
165
|
+
- spec/convert_spec.rb
|
207
166
|
- spec/model_spec.rb
|
208
167
|
- spec/spec_helper.rb
|
data/Guardfile
DELETED
@@ -1,9 +0,0 @@
|
|
1
|
-
# A sample Guardfile
|
2
|
-
# More info at https://github.com/guard/guard#readme
|
3
|
-
|
4
|
-
guard 'rspec', :version => 2 do
|
5
|
-
watch(%r{^spec/.+_spec\.rb$})
|
6
|
-
watch(%r{^lib/command_model/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
7
|
-
watch('spec/spec_helper.rb') { "spec" }
|
8
|
-
end
|
9
|
-
|