can_be 0.2.1 → 0.3.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.
- data/.gitignore +0 -1
- data/CHANGELOG.md +14 -5
- data/Gemfile.lock +56 -0
- data/README.md +30 -48
- data/can_be.gemspec +1 -0
- data/docs/details.md +94 -0
- data/docs/history.md +71 -0
- data/docs/rspec_matcher.md +11 -0
- data/lib/can_be/builder/can_be.rb +24 -5
- data/lib/can_be/builder/can_be_detail.rb +62 -4
- data/lib/can_be/config.rb +30 -5
- data/lib/can_be/model_extensions.rb +3 -3
- data/lib/can_be/processor/instance.rb +106 -11
- data/lib/can_be/rspec/matchers.rb +9 -0
- data/lib/can_be/rspec/matchers/can_be_detail_matcher.rb +26 -0
- data/lib/can_be/rspec/matchers/can_be_matcher.rb +76 -0
- data/lib/can_be/version.rb +1 -1
- data/spec/can_be/config_spec.rb +42 -0
- data/spec/can_be/model_extensions_spec.rb +14 -187
- data/spec/can_be/rspec/matchers/can_be_detail_matcher_spec.rb +11 -0
- data/spec/can_be/rspec/matchers/can_be_matcher_spec.rb +57 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/can_be_detail_history_shared_examples.rb +17 -0
- data/spec/support/can_be_detail_shared_examples.rb +13 -0
- data/spec/support/can_be_history_shared_examples.rb +135 -0
- data/spec/support/can_be_shared_examples.rb +204 -0
- data/spec/support/models.rb +54 -0
- data/spec/support/schema.rb +55 -0
- metadata +43 -3
@@ -1,20 +1,78 @@
|
|
1
|
+
require 'can_be/config'
|
2
|
+
|
1
3
|
module CanBe
|
2
4
|
module Builder
|
3
5
|
class CanBeDetail
|
4
|
-
def self.build(klass, can_be_model)
|
5
|
-
new(klass, can_be_model).
|
6
|
+
def self.build(klass, can_be_model, options = {})
|
7
|
+
new(klass, can_be_model, options).define_functionality
|
6
8
|
end
|
7
9
|
|
8
|
-
def initialize(klass, can_be_model)
|
10
|
+
def initialize(klass, can_be_model, options)
|
9
11
|
@klass = klass
|
10
12
|
@can_be_model = can_be_model
|
13
|
+
@options = parse_options(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def define_functionality
|
17
|
+
define_association
|
18
|
+
define_history
|
11
19
|
end
|
12
20
|
|
13
21
|
def define_association
|
14
22
|
can_be_model = @can_be_model
|
23
|
+
details_name = @options[:details_name]
|
15
24
|
|
16
25
|
@klass.class_eval do
|
17
|
-
has_one can_be_model, as:
|
26
|
+
has_one can_be_model, as: details_name.to_sym, dependent: :destroy
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def define_history
|
31
|
+
return unless keeps_history?
|
32
|
+
|
33
|
+
history_model = @options[:history_model]
|
34
|
+
details_name = @options[:details_name]
|
35
|
+
can_be_model = @can_be_model
|
36
|
+
|
37
|
+
@klass.instance_eval do
|
38
|
+
define_method can_be_model do |*params|
|
39
|
+
begin
|
40
|
+
details = association(details_name).reader(false)
|
41
|
+
return details
|
42
|
+
rescue NoMethodError
|
43
|
+
# this should be for the missing 'association_class' method because the association is broken
|
44
|
+
# so we just want to move on and find the record manually
|
45
|
+
end
|
46
|
+
|
47
|
+
history_model_class = history_model.to_s.camelize.constantize
|
48
|
+
history = history_model_class.where({
|
49
|
+
can_be_details_id: self.id,
|
50
|
+
can_be_details_type: self.class.name.underscore
|
51
|
+
}).first
|
52
|
+
|
53
|
+
can_be_model_class = can_be_model.to_s.camelize.constantize
|
54
|
+
can_be_model_class.find(history.can_be_model_id)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def keeps_history?
|
61
|
+
!@options[:history_model].nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_options(options)
|
65
|
+
default_options = {
|
66
|
+
details_name: Config::DEFAULT_DETAILS_NAME,
|
67
|
+
history_model: nil
|
68
|
+
}
|
69
|
+
|
70
|
+
if options.is_a? Symbol
|
71
|
+
# this is defining the details name
|
72
|
+
default_options.merge details_name: options
|
73
|
+
else
|
74
|
+
# the options must be a hash
|
75
|
+
default_options.merge options
|
18
76
|
end
|
19
77
|
end
|
20
78
|
end
|
data/lib/can_be/config.rb
CHANGED
@@ -1,19 +1,36 @@
|
|
1
1
|
module CanBe
|
2
2
|
class Config
|
3
3
|
DEFAULT_CAN_BE_FIELD = :can_be_type
|
4
|
+
DEFAULT_DETAILS_NAME = :details
|
4
5
|
|
5
|
-
attr_reader :types
|
6
|
+
attr_reader :types, :history_model
|
6
7
|
|
7
|
-
def field_name
|
8
|
-
|
8
|
+
def field_name(name = nil)
|
9
|
+
if name.nil?
|
10
|
+
@field_name || CanBe::Config::DEFAULT_CAN_BE_FIELD
|
11
|
+
else
|
12
|
+
@field_name = name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def details_name(name = nil)
|
17
|
+
if name.nil?
|
18
|
+
@details_name || CanBe::Config::DEFAULT_DETAILS_NAME
|
19
|
+
else
|
20
|
+
@details_name = name
|
21
|
+
end
|
9
22
|
end
|
10
23
|
|
11
24
|
def types=(types)
|
12
25
|
@types = types.map(&:to_s)
|
13
26
|
end
|
14
27
|
|
15
|
-
def default_type
|
16
|
-
|
28
|
+
def default_type(type = nil)
|
29
|
+
if type.nil?
|
30
|
+
@default_type || @types.first
|
31
|
+
else
|
32
|
+
@default_type = type
|
33
|
+
end
|
17
34
|
end
|
18
35
|
|
19
36
|
def parse_options(options = {})
|
@@ -28,5 +45,13 @@ module CanBe
|
|
28
45
|
def add_details_model(can_be_type, model_symbol)
|
29
46
|
self.details[can_be_type] = model_symbol
|
30
47
|
end
|
48
|
+
|
49
|
+
def keep_history_in(history_model)
|
50
|
+
@history_model = history_model
|
51
|
+
end
|
52
|
+
|
53
|
+
def keeps_history?
|
54
|
+
!@history_model.nil?
|
55
|
+
end
|
31
56
|
end
|
32
57
|
end
|
@@ -18,13 +18,13 @@ module CanBe
|
|
18
18
|
can_be_config.types = types
|
19
19
|
can_be_config.parse_options options if options
|
20
20
|
|
21
|
-
can_be_config.instance_eval(&block)if block_given?
|
21
|
+
can_be_config.instance_eval(&block) if block_given?
|
22
22
|
|
23
23
|
CanBe::Builder::CanBe.build(self)
|
24
24
|
end
|
25
25
|
|
26
|
-
def can_be_detail(can_be_model)
|
27
|
-
CanBe::Builder::CanBeDetail.build(self, can_be_model)
|
26
|
+
def can_be_detail(can_be_model, options = {})
|
27
|
+
CanBe::Builder::CanBeDetail.build(self, can_be_model, options)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
@@ -5,20 +5,32 @@ module CanBe
|
|
5
5
|
@model = model
|
6
6
|
@config = model.class.can_be_config
|
7
7
|
@field_name = @config.field_name
|
8
|
+
@details_name = @config.details_name.to_sym
|
9
|
+
@details_id = "#{@details_name}_id".to_sym
|
10
|
+
@details_type = "#{@details_name}_type".to_sym
|
11
|
+
set_cleaning_defaults
|
8
12
|
end
|
9
13
|
|
10
14
|
def boolean_eval(t)
|
11
|
-
field_value == t
|
15
|
+
field_value.to_s == t.to_s
|
12
16
|
end
|
13
17
|
|
14
|
-
def update_field(t,
|
18
|
+
def update_field(t, options = {})
|
19
|
+
@original_details = @model.send(@details_name)
|
20
|
+
@force_history_removal = options[:force_history_removal] if options.has_key?(:force_history_removal)
|
21
|
+
|
22
|
+
save = options.has_key?(:save) ? options[:save] : false
|
23
|
+
|
15
24
|
if save
|
16
|
-
original_details = @model.details
|
17
25
|
@model.update_attributes(@field_name => t)
|
18
|
-
original_details.destroy unless original_details.class == @model.details.class
|
19
26
|
else
|
20
27
|
self.field_value = t
|
21
28
|
end
|
29
|
+
|
30
|
+
if block_given?
|
31
|
+
yield(@model.send(@details_name))
|
32
|
+
@model.send(@details_name).save if save
|
33
|
+
end
|
22
34
|
end
|
23
35
|
|
24
36
|
def field_value=(t)
|
@@ -35,26 +47,109 @@ module CanBe
|
|
35
47
|
end
|
36
48
|
|
37
49
|
def initialize_details
|
38
|
-
set_details(field_value.to_sym) if has_details? && !@model.details_id
|
50
|
+
set_details(field_value.to_sym) if has_details? && !@model.send(@details_id)
|
51
|
+
end
|
52
|
+
|
53
|
+
def clean_details
|
54
|
+
if @original_details && @original_details.class != @model.send(@details_name).class
|
55
|
+
if @config.keeps_history?
|
56
|
+
if @force_history_removal
|
57
|
+
@original_details.destroy
|
58
|
+
destroy_history(@original_details.class.name.underscore)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
@original_details.destroy
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
set_cleaning_defaults
|
66
|
+
end
|
67
|
+
|
68
|
+
def set_cleaning_defaults
|
69
|
+
@original_details = nil
|
70
|
+
@force_history_removal = false
|
71
|
+
end
|
72
|
+
|
73
|
+
def save_history
|
74
|
+
history_model_class.create({
|
75
|
+
can_be_model_id: @model.id,
|
76
|
+
can_be_type: field_value,
|
77
|
+
can_be_details_id: @model.send(@details_name).id,
|
78
|
+
can_be_details_type: details_class_name(field_value)
|
79
|
+
}) unless history_model_for(field_value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def destroy_histories
|
83
|
+
histories = history_model_class.where(can_be_model_id: @model.id)
|
84
|
+
|
85
|
+
destroy_details_history(histories)
|
86
|
+
histories.destroy_all
|
87
|
+
end
|
88
|
+
|
89
|
+
def destroy_history(details_type)
|
90
|
+
history_model_class.where(can_be_model_id: @model.id, can_be_details_type: details_type).destroy_all
|
39
91
|
end
|
40
92
|
|
41
93
|
private
|
42
94
|
def has_details?
|
43
|
-
@model.respond_to?(
|
95
|
+
@model.respond_to?(@details_name) && @model.respond_to?(@details_id) && @model.respond_to?(@details_type)
|
44
96
|
end
|
45
97
|
|
46
98
|
def set_details(t)
|
47
99
|
return unless has_details?
|
48
100
|
|
49
|
-
|
101
|
+
if details_class_name(t)
|
102
|
+
if @config.keeps_history?
|
103
|
+
set_history_details_for(t)
|
104
|
+
else
|
105
|
+
@model.send("#{@details_name}=", details_for(t))
|
106
|
+
end
|
107
|
+
else
|
108
|
+
@model.send("#{@details_id}=", nil)
|
109
|
+
@model.send("#{@details_type}=", nil)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def destroy_details_history(histories)
|
114
|
+
histories.each do |h|
|
115
|
+
details_record = details_class(h.can_be_type).where(id: h.can_be_details_id).first
|
116
|
+
details_record.destroy if details_record
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def set_history_details_for(t)
|
121
|
+
history_model = history_model_for(t)
|
122
|
+
|
123
|
+
if history_model
|
124
|
+
@model.send("#{@details_name}=", details_for(t, history_model.can_be_details_id))
|
125
|
+
else
|
126
|
+
@model.send("#{@details_name}=", details_for(t))
|
127
|
+
end
|
128
|
+
end
|
50
129
|
|
51
|
-
|
52
|
-
|
130
|
+
def details_for(t, details_id = nil)
|
131
|
+
if details_id.nil?
|
132
|
+
details_class(t).new
|
53
133
|
else
|
54
|
-
|
55
|
-
@model.details_type = nil
|
134
|
+
details_class(t).find(details_id)
|
56
135
|
end
|
57
136
|
end
|
137
|
+
|
138
|
+
def history_model_for(t)
|
139
|
+
history_model_class.where(can_be_model_id: @model.id, can_be_type: t).first
|
140
|
+
end
|
141
|
+
|
142
|
+
def details_class_name(t)
|
143
|
+
@config.details[t.to_sym]
|
144
|
+
end
|
145
|
+
|
146
|
+
def details_class(t)
|
147
|
+
details_class_name(t).to_s.camelize.constantize if details_class_name(t)
|
148
|
+
end
|
149
|
+
|
150
|
+
def history_model_class
|
151
|
+
@config.history_model.to_s.camelize.constantize
|
152
|
+
end
|
58
153
|
end
|
59
154
|
end
|
60
155
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
RSpec::Matchers.define :implement_can_be_detail do |can_be_type, details_name|
|
2
|
+
match do |actual|
|
3
|
+
reflections = actual.reflect_on_all_associations(:has_one)
|
4
|
+
details_name_to_match = details_name || CanBe::Config::DEFAULT_DETAILS_NAME
|
5
|
+
|
6
|
+
matched = false
|
7
|
+
|
8
|
+
reflections.each do |reflection|
|
9
|
+
as_matches = reflection.options[:as] == details_name_to_match
|
10
|
+
|
11
|
+
if reflection.name == can_be_type.to_sym && as_matches
|
12
|
+
matched = true
|
13
|
+
break
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
matched
|
18
|
+
end
|
19
|
+
|
20
|
+
failure_message_for_should do |actual|
|
21
|
+
failure_message = "expected that #{actual.name} would implement can_be_detail with a can_be_type of #{can_be_type.to_sym}"
|
22
|
+
failure_message += " and a details_name of #{details_name.to_sym}" if details_name
|
23
|
+
failure_message += "."
|
24
|
+
failure_message
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
RSpec::Matchers.define :implement_can_be do |*expected_types|
|
2
|
+
match do |actual|
|
3
|
+
@config = actual.can_be_config
|
4
|
+
|
5
|
+
types_match?(expected_types || []) &&
|
6
|
+
default_type_matches? &&
|
7
|
+
field_name_matches? &&
|
8
|
+
details_name_matches? &&
|
9
|
+
details_matches?
|
10
|
+
end
|
11
|
+
|
12
|
+
failure_message_for_should do |actual|
|
13
|
+
failure_message = "expected that #{actual.name} would implement can_be with the following types: #{expected_types.map(&:to_sym).join(", ")}."
|
14
|
+
failure_message += " With the default type of #{@expected_default_type.to_sym}." if @expected_default_type
|
15
|
+
failure_message += " With the field name of #{@expected_field_name.to_sym}." if @expected_field_name
|
16
|
+
failure_message += " With the details name of #{@expected_details_name.to_sym}." if @expected_details_name
|
17
|
+
|
18
|
+
@expected_details.each do |can_be_type, model|
|
19
|
+
failure_message += " With the details of can_be_type: #{can_be_type.to_sym} & model: #{model.to_sym}."
|
20
|
+
end if @expected_details
|
21
|
+
|
22
|
+
failure_message
|
23
|
+
end
|
24
|
+
|
25
|
+
chain :with_default_type do |default_type|
|
26
|
+
@expected_default_type = default_type
|
27
|
+
end
|
28
|
+
|
29
|
+
chain :with_field_name do |field_name|
|
30
|
+
@expected_field_name = field_name
|
31
|
+
end
|
32
|
+
|
33
|
+
chain :with_details_name do |details_name|
|
34
|
+
@expected_details_name = details_name
|
35
|
+
end
|
36
|
+
|
37
|
+
chain :and_has_details do |can_be_type, model|
|
38
|
+
@expected_details = {} unless @expected_details
|
39
|
+
@expected_details[can_be_type.to_sym] = model.to_sym
|
40
|
+
end
|
41
|
+
|
42
|
+
def types_match?(expected_types)
|
43
|
+
return true unless expected_types
|
44
|
+
config_types = @config.types || []
|
45
|
+
expected_types.map(&:to_s).sort == config_types.sort
|
46
|
+
end
|
47
|
+
|
48
|
+
def default_type_matches?
|
49
|
+
return true unless @expected_default_type
|
50
|
+
@config.default_type.to_s == @expected_default_type.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
def field_name_matches?
|
54
|
+
return true unless @expected_field_name
|
55
|
+
@config.field_name.to_s == @expected_field_name.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def details_name_matches?
|
59
|
+
return true unless @expected_details_name
|
60
|
+
@config.details_name.to_sym == @expected_details_name.to_sym
|
61
|
+
end
|
62
|
+
|
63
|
+
def details_matches?
|
64
|
+
return true unless @expected_details
|
65
|
+
|
66
|
+
@config.details.each do |can_be_type, model|
|
67
|
+
return false unless @expected_details[can_be_type] == model
|
68
|
+
end
|
69
|
+
|
70
|
+
@expected_details.each do |can_be_type, model|
|
71
|
+
return false unless @config.details[can_be_type] == model
|
72
|
+
end
|
73
|
+
|
74
|
+
return true
|
75
|
+
end
|
76
|
+
end
|
data/lib/can_be/version.rb
CHANGED
data/spec/can_be/config_spec.rb
CHANGED
@@ -5,6 +5,12 @@ describe CanBe::Config do
|
|
5
5
|
it "defines a default field name" do
|
6
6
|
subject.field_name.should == :can_be_type
|
7
7
|
end
|
8
|
+
|
9
|
+
it "sets the field name" do
|
10
|
+
field_name = :custom_field_name
|
11
|
+
subject.field_name field_name
|
12
|
+
subject.field_name.should == field_name
|
13
|
+
end
|
8
14
|
end
|
9
15
|
|
10
16
|
context "#types" do
|
@@ -22,6 +28,24 @@ describe CanBe::Config do
|
|
22
28
|
it "returns the first type by default" do
|
23
29
|
subject.default_type.should == "a"
|
24
30
|
end
|
31
|
+
|
32
|
+
it "sets the default type" do
|
33
|
+
default_type = :custom_default_type
|
34
|
+
subject.default_type default_type
|
35
|
+
subject.default_type.should == default_type
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "#details_name" do
|
40
|
+
it "defines a default details name" do
|
41
|
+
subject.details_name.should == :details
|
42
|
+
end
|
43
|
+
|
44
|
+
it "sets the details name" do
|
45
|
+
details_name = :custom_details_name
|
46
|
+
subject.details_name details_name
|
47
|
+
subject.details_name.should == details_name
|
48
|
+
end
|
25
49
|
end
|
26
50
|
|
27
51
|
context "#parse_options" do
|
@@ -58,5 +82,23 @@ describe CanBe::Config do
|
|
58
82
|
subject.details[:type1].should == :config_spec_model2
|
59
83
|
end
|
60
84
|
end
|
85
|
+
|
86
|
+
context "#keep_history_in" do
|
87
|
+
it "keeps the history model name" do
|
88
|
+
subject.keep_history_in :history_model
|
89
|
+
subject.history_model.should == :history_model
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "#keeps_history?" do
|
94
|
+
it "keeps history when history_model is specified" do
|
95
|
+
subject.keep_history_in :history_model
|
96
|
+
subject.keeps_history?.should be_true
|
97
|
+
end
|
98
|
+
|
99
|
+
it "doesn't keep history when history_model isn't specified" do
|
100
|
+
subject.keeps_history?.should be_false
|
101
|
+
end
|
102
|
+
end
|
61
103
|
end
|
62
104
|
|