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.
@@ -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