can_be 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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).define_association
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: :details, dependent: :destroy
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
@@ -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
- @field_name || CanBe::Config::DEFAULT_CAN_BE_FIELD
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
- @default_type || @types.first
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, save = false)
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?(:details) && @model.respond_to?(:details_id) && @model.respond_to?(:details_type)
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
- classname = @config.details[t.to_sym]
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
- if classname
52
- @model.details = classname.to_s.camelize.constantize.new
130
+ def details_for(t, details_id = nil)
131
+ if details_id.nil?
132
+ details_class(t).new
53
133
  else
54
- @model.details_id = nil
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,9 @@
1
+ require 'can_be/rspec/matchers/can_be_detail_matcher'
2
+ require 'can_be/rspec/matchers/can_be_matcher'
3
+
4
+ module CanBe
5
+ module RSpec
6
+ module Matchers
7
+ end
8
+ end
9
+ 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
@@ -1,3 +1,3 @@
1
1
  module CanBe
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -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