mschuerig-enumerate_by 0.4.2

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.
@@ -0,0 +1,109 @@
1
+ module EnumerateBy
2
+ module Extensions #:nodoc:
3
+ # Adds a set of helpers for using enumerations in associations, including
4
+ # named scopes and assignment via enumerators.
5
+ #
6
+ # The examples below assume the following models have been defined:
7
+ #
8
+ # class Color < ActiveRecord::Base
9
+ # enumerate_by :name
10
+ #
11
+ # bootstrap(
12
+ # {:id => 1, :name => 'red'},
13
+ # {:id => 2, :name => 'blue'},
14
+ # {:id => 3, :name => 'green'}
15
+ # )
16
+ # end
17
+ #
18
+ # class Car < ActiveRecord::Base
19
+ # belongs_to :color
20
+ # end
21
+ #
22
+ # == Named scopes
23
+ #
24
+ # A pair of named scopes are generated for each +belongs_to+ association
25
+ # that is identified by an enumeration. In this case, the following
26
+ # named scopes get generated:
27
+ # * +with_color+ / +with_colors+ - Finds all cars with the given color(s)
28
+ # * +without_color+ / +without_colors+ - Finds all cars without the given color(s)
29
+ #
30
+ # For example,
31
+ #
32
+ # Car.with_color('red') # Cars with the color name "red"
33
+ # Car.without_color('red') # Cars without the color name "red"
34
+ # Car.with_colors('red', 'blue') # Cars with either the color names "red" or "blue"
35
+ #
36
+ # == Association assignment
37
+ #
38
+ # Normally, +belongs_to+ associations are assigned with either the actual
39
+ # record or through its primary key. When used with enumerations, support
40
+ # is added for assigning these associations through the enumerators
41
+ # defined for the class.
42
+ #
43
+ # For example,
44
+ #
45
+ # # With valid enumerator
46
+ # car = Car.new # => #<Car id: nil, color_id: nil>
47
+ # car.color = 'red'
48
+ # car.color_id # => 1
49
+ # car.color # => #<Color id: 1, name: "red">
50
+ #
51
+ # # With invalid enumerator
52
+ # car = Car.new # => #<Car id: nil, color_id: nil>
53
+ # car.color = 'invalid'
54
+ # car.color_id # => nil
55
+ # car.color # => nil
56
+ #
57
+ # In the above example, the actual Color association is automatically
58
+ # looked up by finding the Color record identified by the enumerator the
59
+ # given enumerator ("red" in this case).
60
+ module Associations
61
+ def self.extended(base) #:nodoc:
62
+ class << base
63
+ alias_method_chain :belongs_to, :enumerations
64
+ end
65
+ end
66
+
67
+ # Adds support for belongs_to and enumerations
68
+ def belongs_to_with_enumerations(association_id, options = {})
69
+ belongs_to_without_enumerations(association_id, options)
70
+
71
+ # Override accessor if class is valid enumeration
72
+ reflection = reflections[association_id.to_sym]
73
+ if !reflection.options[:polymorphic] && (reflection.klass < ActiveRecord::Base) && reflection.klass.enumeration?
74
+ name = reflection.name
75
+ primary_key_name = reflection.primary_key_name
76
+ class_name = reflection.class_name
77
+ klass = reflection.klass
78
+
79
+ # Inclusion scopes
80
+ %W(with_#{name} with_#{name.to_s.pluralize}).each do |scope_name|
81
+ named_scope scope_name.to_sym, lambda {|*enumerators| {
82
+ :conditions => {primary_key_name => klass.find_all_by_enumerator!(enumerators).map(&:id)}
83
+ }}
84
+ end
85
+
86
+ # Exclusion scopes
87
+ %W(without_#{name} without_#{name.to_s.pluralize}).each do |scope_name|
88
+ named_scope scope_name.to_sym, lambda {|*enumerators| {
89
+ :conditions => ["#{primary_key_name} NOT IN (?)", klass.find_all_by_enumerator!(enumerators).map(&:id)]
90
+ }}
91
+ end
92
+
93
+ # Hook in shortcut writer
94
+ define_method("#{name}_with_enumerators=") do |new_value|
95
+ send("#{name}_without_enumerators=", new_value.is_a?(klass) ? new_value : klass.find_by_enumerator(new_value))
96
+ end
97
+ alias_method_chain "#{name}=", :enumerators
98
+
99
+ # Track the association
100
+ enumeration_associations[primary_key_name.to_s] = name.to_s
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ ActiveRecord::Base.class_eval do
108
+ extend EnumerateBy::Extensions::Associations
109
+ end
@@ -0,0 +1,130 @@
1
+ module EnumerateBy
2
+ module Extensions #:nodoc:
3
+ # Adds support for using enumerators in dynamic finders and conditions.
4
+ # For example suppose the following models are defined:
5
+ #
6
+ # class Color < ActiveRecord::Base
7
+ # enumerate_by :name
8
+ #
9
+ # bootstrap(
10
+ # {:id => 1, :name => 'red'},
11
+ # {:id => 2, :name => 'blue'},
12
+ # {:id => 3, :name => 'green'}
13
+ # )
14
+ # end
15
+ #
16
+ # class Car < ActiveRecord::Base
17
+ # belongs_to :color
18
+ # end
19
+ #
20
+ # Normally, looking up all cars associated with a particular color
21
+ # requires either a join or knowing the id of the color upfront:
22
+ #
23
+ # Car.find(:all, :joins => :color, :conditions => {:colors => {:name => 'red}})
24
+ # Car.find_by_color_id(1)
25
+ #
26
+ # Instead of doing this manually, the color can be referenced directly
27
+ # via its enumerator like so:
28
+ #
29
+ # # With dynamic finders
30
+ # Car.find_by_color('red')
31
+ #
32
+ # # With conditions
33
+ # Car.all(:conditions => {:color => 'red'})
34
+ #
35
+ # # With updates
36
+ # Car.update_all(:color => 'red')
37
+ #
38
+ # In the above examples, +color+ is essentially treated like a normal
39
+ # attribute on the class, instead triggering the associated Color record
40
+ # to be looked up and replacing the condition with a +color_id+ condition.
41
+ #
42
+ # *Note* that this does not add an additional join on the +colors+ table
43
+ # since the lookup of the color's id should be relatively fast when it's
44
+ # cached in-memory.
45
+ module BaseConditions
46
+ def self.extended(base) #:nodoc:
47
+ class << base
48
+ alias_method_chain :construct_attributes_from_arguments, :enumerations
49
+ alias_method_chain :sanitize_sql_hash_for_conditions, :enumerations
50
+ alias_method_chain :sanitize_sql_hash_for_assignment, :enumerations
51
+ alias_method_chain :all_attributes_exists?, :enumerations
52
+ end
53
+ end
54
+
55
+ # Add support for dynamic finders
56
+ def construct_attributes_from_arguments_with_enumerations(attribute_names, arguments)
57
+ attributes = construct_attributes_from_arguments_without_enumerations(attribute_names, arguments)
58
+ attribute_names.each_with_index do |name, idx|
59
+ if options = enumerator_options_for(name, arguments[idx])
60
+ attributes.delete(name)
61
+ attributes.merge!(options)
62
+ end
63
+ end
64
+
65
+ attributes
66
+ end
67
+
68
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
69
+ def sanitize_sql_hash_for_conditions_with_enumerations(*args)
70
+ replace_enumerations_in_hash(args.first)
71
+ sanitize_sql_hash_for_conditions_without_enumerations(*args)
72
+ end
73
+
74
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
75
+ def sanitize_sql_hash_for_assignment_with_enumerations(*args)
76
+ replace_enumerations_in_hash(args.first, false)
77
+ sanitize_sql_hash_for_assignment_without_enumerations(*args)
78
+ end
79
+
80
+ # Make sure dynamic finders don't fail since it won't find the association
81
+ # name in its columns
82
+ def all_attributes_exists_with_enumerations?(attribute_names)
83
+ exists = all_attributes_exists_without_enumerations?(attribute_names)
84
+ exists ||= attribute_names.all? do |name|
85
+ column_methods_hash.include?(name.to_sym) || reflect_on_enumeration(name)
86
+ end
87
+ end
88
+
89
+ private
90
+ # Finds all of the attributes that are enumerations and replaces them
91
+ # with the correct enumerator id
92
+ def replace_enumerations_in_hash(attrs, allow_multiple = true) #:nodoc:
93
+ attrs.each do |attr, value|
94
+ if options = enumerator_options_for(attr, value, allow_multiple)
95
+ attrs.delete(attr)
96
+ attrs.merge!(options)
97
+ end
98
+ end
99
+ end
100
+
101
+ # Generates the enumerator lookup options for the given association
102
+ # name and enumerator value. If the association is *not* for an
103
+ # enumeration, then this will return nil.
104
+ def enumerator_options_for(name, enumerator, allow_multiple = true)
105
+ if reflection = reflect_on_enumeration(name)
106
+ klass = reflection.klass
107
+ attribute = reflection.primary_key_name
108
+ id = if allow_multiple && enumerator.is_a?(Array)
109
+ enumerator.map {|enumerator| klass.find_by_enumerator!(enumerator).id}
110
+ else
111
+ klass.find_by_enumerator!(enumerator).id
112
+ end
113
+
114
+ {attribute => id}
115
+ end
116
+ end
117
+
118
+ # Attempts to find an association with the given name *and* represents
119
+ # an enumeration
120
+ def reflect_on_enumeration(name)
121
+ reflection = reflect_on_association(name.to_sym)
122
+ reflection if reflection && reflection.klass.enumeration?
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ ActiveRecord::Base.class_eval do
129
+ extend EnumerateBy::Extensions::BaseConditions
130
+ end
@@ -0,0 +1,117 @@
1
+ module EnumerateBy
2
+ module Extensions #:nodoc:
3
+ # Adds support for automatically converting enumeration attributes to the
4
+ # value represented by them.
5
+ #
6
+ # == Examples
7
+ #
8
+ # Suppose the following models are defined:
9
+ #
10
+ # class Color < ActiveRecord::Base
11
+ # enumerate_by :name
12
+ #
13
+ # bootstrap(
14
+ # {:id => 1, :name => 'red'},
15
+ # {:id => 2, :name => 'blue'},
16
+ # {:id => 3, :name => 'green'}
17
+ # )
18
+ # end
19
+ #
20
+ # class Car < ActiveRecord::Base
21
+ # belongs_to :color
22
+ # end
23
+ #
24
+ # Given the above, the enumerator for the car will be automatically
25
+ # used for serialization instead of the foreign key like so:
26
+ #
27
+ # car = Car.create(:color => 'red') # => #<Car id: 1, color_id: 1>
28
+ # car.to_xml # => "<car><id type=\"integer\">1</id><color>red</color></car>"
29
+ # car.to_json # => "{id: 1, color: \"red\"}"
30
+ #
31
+ # == Conversion options
32
+ #
33
+ # The actual conversion of enumeration associations can be controlled
34
+ # using the following options:
35
+ #
36
+ # car.to_json # => "{id: 1, color: \"red\"}"
37
+ # car.to_json(:enumerations => false) # => "{id: 1, color_id: 1}"
38
+ # car.to_json(:only => [:color_id]) # => "{color_id: 1}"
39
+ # car.to_json(:only => [:color]) # => "{color: \"red\"}"
40
+ # car.to_json(:include => :color) # => "{id: 1, color_id: 1, color: {id: 1, name: \"red\"}}"
41
+ #
42
+ # As can be seen from above, enumeration attributes can either be treated
43
+ # as pseudo-attributes on the record or its actual association.
44
+ module Serializer
45
+ def self.included(base) #:nodoc:
46
+ base.class_eval do
47
+ alias_method_chain :serializable_attribute_names, :enumerations
48
+ alias_method_chain :serializable_record, :enumerations
49
+ end
50
+ end
51
+
52
+ # Automatically converted enumeration attributes to their association
53
+ # names so that they *appear* as attributes
54
+ def serializable_attribute_names_with_enumerations
55
+ attribute_names = serializable_attribute_names_without_enumerations
56
+
57
+ # Adjust the serializable attributes by converting primary keys for
58
+ # enumeration associations to their association name (where possible)
59
+ if convert_enumerations?
60
+ @only_attributes = Array(options[:only]).map(&:to_s)
61
+ @include_associations = Array(options[:include]).map(&:to_s)
62
+
63
+ attribute_names.map! {|attribute| enumeration_association_for(attribute) || attribute}
64
+ attribute_names |= @record.class.enumeration_associations.values & @only_attributes
65
+ attribute_names.sort!
66
+ attribute_names -= options[:except].map(&:to_s) unless options[:only]
67
+ end
68
+
69
+ attribute_names
70
+ end
71
+
72
+ # Automatically casts enumerations to their public values
73
+ def serializable_record_with_enumerations
74
+ returning(serializable_record = serializable_record_without_enumerations) do
75
+ serializable_record.each do |attribute, value|
76
+ # Typecast to enumerator value
77
+ serializable_record[attribute] = value.enumerator if typecast_to_enumerator?(attribute, value)
78
+ end if convert_enumerations?
79
+ end
80
+ end
81
+
82
+ private
83
+ # Should enumeration attributes be automatically converted based on
84
+ # the serialization configuration
85
+ def convert_enumerations?
86
+ options[:enumerations] != false
87
+ end
88
+
89
+ # Should the given attribute be converted to the actual enumeration?
90
+ def convert_to_enumeration?(attribute)
91
+ !@only_attributes.include?(attribute)
92
+ end
93
+
94
+ # Gets the association name for the given enumeration attribute, if
95
+ # one exists
96
+ def enumeration_association_for(attribute)
97
+ association = @record.class.enumeration_associations[attribute]
98
+ association if association && convert_to_enumeration?(attribute) && !include_enumeration?(association)
99
+ end
100
+
101
+ # Is the given enumeration attribute being included as a whole record
102
+ # instead of just an individual attribute?
103
+ def include_enumeration?(association)
104
+ @include_associations.include?(association)
105
+ end
106
+
107
+ # Should the given value be typecasted to its enumerator value?
108
+ def typecast_to_enumerator?(association, value)
109
+ value.is_a?(ActiveRecord::Base) && value.class.enumeration? && !include_enumeration?(association)
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ ActiveRecord::Serialization::Serializer.class_eval do
116
+ include EnumerateBy::Extensions::Serializer
117
+ end
@@ -0,0 +1,41 @@
1
+ module EnumerateBy
2
+ module Extensions #:nodoc:
3
+ module XmlSerializer #:nodoc:
4
+ # Adds support for xml serialization of enumeration associations as
5
+ # attributes
6
+ module Attribute
7
+ def self.included(base) #:nodoc:
8
+ base.class_eval do
9
+ alias_method_chain :compute_type, :enumerations
10
+ alias_method_chain :compute_value, :enumerations
11
+ end
12
+ end
13
+
14
+ protected
15
+ # Enumerator types are always strings
16
+ def compute_type_with_enumerations
17
+ enumeration_association? ? :string : compute_type_without_enumerations
18
+ end
19
+
20
+ # Gets the real value representing the enumerator
21
+ def compute_value_with_enumerations
22
+ if enumeration_association?
23
+ association = @record.send(name)
24
+ association.enumerator if association
25
+ else
26
+ compute_value_without_enumerations
27
+ end
28
+ end
29
+
30
+ # Is this attribute defined by an enumeration association?
31
+ def enumeration_association?
32
+ @enumeration_association ||= @record.enumeration_associations.value?(name)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ ActiveRecord::XmlSerializer::Attribute.class_eval do
40
+ include EnumerateBy::Extensions::XmlSerializer::Attribute
41
+ end
@@ -0,0 +1 @@
1
+ require 'enumerate_by'
@@ -0,0 +1,4 @@
1
+ class Car < ActiveRecord::Base
2
+ belongs_to :color
3
+ belongs_to :feature, :polymorphic => true
4
+ end
@@ -0,0 +1,3 @@
1
+ class Color < ActiveRecord::Base
2
+ enumerate_by :name
3
+ end
@@ -0,0 +1,12 @@
1
+ class CreateColors < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :colors do |t|
4
+ t.string :name, :null => false
5
+ t.string :html
6
+ end
7
+ end
8
+
9
+ def self.down
10
+ drop_table :colors
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ class CreateCars < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :cars do |t|
4
+ t.string :name
5
+ t.references :color
6
+ t.references :feature, :class_name => 'Color', :polymorphic => true
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :cars
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ module Factory
2
+ # Build actions for the model
3
+ def self.build(model, &block)
4
+ name = model.to_s.underscore
5
+
6
+ define_method("#{name}_attributes", block)
7
+ define_method("valid_#{name}_attributes") {|*args| valid_attributes_for(model, *args)}
8
+ define_method("new_#{name}") {|*args| new_record(model, *args)}
9
+ define_method("create_#{name}") {|*args| create_record(model, *args)}
10
+ end
11
+
12
+ # Get valid attributes for the model
13
+ def valid_attributes_for(model, attributes = {})
14
+ name = model.to_s.underscore
15
+ send("#{name}_attributes", attributes)
16
+ attributes.stringify_keys!
17
+ attributes
18
+ end
19
+
20
+ # Build an unsaved record
21
+ def new_record(model, *args)
22
+ attributes = valid_attributes_for(model, *args)
23
+ record = model.new(attributes)
24
+ attributes.each {|attr, value| record.send("#{attr}=", value) if model.accessible_attributes && !model.accessible_attributes.include?(attr) || model.protected_attributes && model.protected_attributes.include?(attr)}
25
+ record
26
+ end
27
+
28
+ # Build and save/reload a record
29
+ def create_record(model, *args)
30
+ record = new_record(model, *args)
31
+ record.save!
32
+ record.reload
33
+ record
34
+ end
35
+
36
+ build Car do |attributes|
37
+ attributes[:color] = create_color unless attributes.include?(:color)
38
+ attributes.reverse_merge!(
39
+ :name => 'Ford Mustang'
40
+ )
41
+ end
42
+
43
+ build Color do |attributes|
44
+ attributes.reverse_merge!(
45
+ :name => 'red'
46
+ )
47
+ end
48
+ end