piggyback 0.2.4 → 0.3.0b

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ = 0.3.0b / 2011-08-13
2
+
3
+ * Changed API to be attribute-based
4
+
1
5
  = 0.2.4 / 2011-03-24
2
6
 
3
7
  * Changed join type from INNER to OUTER
data/Manifest CHANGED
@@ -1,10 +1,10 @@
1
1
  CHANGELOG
2
2
  MIT-LICENSE
3
+ Manifest
3
4
  README.markdown
4
5
  Rakefile
5
6
  lib/piggyback.rb
6
- piggyback.gemspec
7
+ piggyback.tmproj
7
8
  spec/models.rb
8
9
  spec/piggyback_spec.rb
9
10
  spec/spec_helper.rb
10
- Manifest
data/README.markdown CHANGED
@@ -4,51 +4,51 @@ An extension for piggybacking of ActiveRecord™ models.
4
4
 
5
5
  ### What is Piggybacking?
6
6
 
7
- Piggybacking refers to the technique of dynamically including attributes from an associated object into the master object. This is achieved by joining the associated object's table in a database query and selecting the attributes that should be included with the parent object.
7
+ Piggybacking refers to the technique of dynamically including attributes from an associated model. This is achieved by joining the associated model in a database query and selecting the attributes that should be included with the parent object.
8
8
 
9
9
  This is best illustrated in an example. Consider these models:
10
10
 
11
- class Author < ActiveRecord::Base
11
+ class User < ActiveRecord::Base
12
12
  has_many :posts
13
13
  end
14
14
 
15
15
  class Post < ActiveRecord::Base
16
- belongs_to :author
16
+ belongs_to :user
17
17
  end
18
18
 
19
19
  ActiveRecord supports piggybacking simply by joining the associated table and selecting columns from it:
20
20
 
21
- post = Post.select('posts.*, author.name AS author_name') \
22
- .joins("JOIN authors ON posts.author_id = authors.id") \
21
+ post = Post.select('posts.*, user.name AS user_name') \
22
+ .joins("JOIN users ON posts.user_id = users.id") \
23
23
  .first
24
24
 
25
- post.title # => "Why piggybacking in ActiveRecord is flawed"
26
- post.author_name # => "Alec Smart"
25
+ post.title # => "Why piggybacking in ActiveRecord is flawed"
26
+ post.user_name # => "Alec Smart"
27
27
 
28
- As you can see, the `name` attribute from `Author` is treated as if it were an attribute of `Post`. ActiveRecord dynamically determines a model's attributes from the result set returned by the database. Every column in the result set becomes an attribute of the instantiated ActiveRecord objects. Whether the columns originate from the model's own or from a foreign table doesn't make a difference.
28
+ As you can see, the `name` attribute from `User` is treated as if it were an attribute of `Post`. ActiveRecord dynamically determines a model's attributes from the result set returned by the database. Every column in the result set becomes an attribute of the instantiated ActiveRecord objects. Whether the columns originate from the model's own or from a foreign table doesn't make a difference.
29
29
 
30
30
  Or so it seems. Actually there is a drawback which becomes obvious when we select non-string columns:
31
31
 
32
- post = Post.select('posts.*, author.birthday AS author_birthday, author.rating AS author_rating') \
33
- .joins("JOIN authors ON posts.author_id = authors.id") \
32
+ post = Post.select('posts.*, user.birthday AS user_birthday, user.rating AS user_rating') \
33
+ .joins("JOIN users ON posts.user_id = users.id") \
34
34
  .first
35
35
 
36
- post.author_birthday # => "2011-03-01"
37
- post.author_rating # => "4.5"
36
+ post.user_birthday # => "2011-03-01"
37
+ post.user_rating # => "4.5"
38
38
 
39
- Any attributes originating from the `authors` table are treated as strings instead of being automatically type-casted as we would expect. The database returns result sets as plain text and ActiveRecord needs to obtain type information separately from the table schema in order to do its type-casting magic. Unfortunately, a model only knows about the columns types in its own table, so type-casting doesn't work with columns selected from foreign tables.
39
+ Any attributes originating from the `users` table are treated as strings instead of being automatically type-casted as we would expect. The database returns result sets as plain text and ActiveRecord needs to obtain type information separately from the table schema in order to do its type-casting magic. Unfortunately, a model only knows about the columns types in its own table, so type-casting doesn't work with columns selected from foreign tables.
40
40
 
41
41
  We could work around this by defining attribute reader methods in the `Post` model that implicitly convert the values:
42
42
 
43
43
  class Post < ActiveRecord::Base
44
- belongs_to :author
44
+ belongs_to :user
45
45
 
46
- def author_birthday
47
- Date.parse(read_attribute(:author_birthday))
46
+ def user_birthday
47
+ Date.parse(read_attribute(:user_birthday))
48
48
  end
49
49
 
50
- def author_rating
51
- read_attribute(:author_rating).to_f
50
+ def user_rating
51
+ read_attribute(:user_rating).to_f
52
52
  end
53
53
  end
54
54
 
@@ -62,41 +62,54 @@ Piggyback introduces the `piggybacks` directive which allows us to easily define
62
62
  You simply declare which association you want to piggyback and how the attribute names should be mapped:
63
63
 
64
64
  class Post < ActiveRecord::Base
65
- belongs_to :author
66
- piggybacks :author, :birthday => :author_birthday, :rating => :author_rating
65
+ belongs_to :user
66
+
67
+ piggyback_attr :name, :email , :from => :user
68
+ piggyback_attr :user_rating , :from => :user, :source => :rating
69
+ piggyback_attr :member_since , :from => :user, :source => :created_at
67
70
  end
68
71
 
69
72
  Now you can do the following:
70
73
 
71
- post = Post.piggyback(:author).first
72
-
73
- post.author_birthday # => Tue, 01 Mar 2011
74
- post.author_rating # => 4.5
74
+ posts = Post.piggiback(:name, :email, :user_rating, :member_since)
75
+ post = posts.first
76
+
77
+ post.name # => "John Doe"
78
+ post.user_rating # => 4.5
79
+ post.member_since # => Tue, 01 Mar 2011
75
80
 
76
81
  The type-casting works with any type of attribute, even with serialized ones.
77
82
 
83
+ Since we selected all attributes from the user, we could alternatively have used:
84
+
85
+ posts = Post.piggiback(:from => :user)
86
+
87
+ Or we could have as well selected all piggiback attributes simply with:
88
+
89
+ posts = Post.piggiback
90
+
78
91
  As you can see, the `piggibacks` statement replaces the `joins` and `select` parts of the query. Using it is optional but makes life easier. In certain situations however, you may want to select columns by hand or use another kind of join for related table. By default `piggibacks` uses `OUTER JOIN` in order to include both records that have an associated record and ones that don't.
79
92
 
80
93
  Of course, `piggibacks` plays nice with Arel and you can add additional `joins`, `select` and other statements as you like, for example:
81
94
 
82
- Post.select('posts.title, posts.body').piggibacks(:author).where(:published => true)
95
+ Post.select('posts.id, posts.body').piggiback(:name, :email).where(:published => true)
83
96
 
84
97
  __Please note:__ If you want to restrict the columns selected from the master table as in the example above, you have to do so _before_ the `piggibacks` statement. Otherwise it will insert the select-all wildcard `SELECT posts.*` rendering your column selection useless.
85
98
 
86
99
  If you don't need to map the attribute names of the piggybacked model, you can simply do:
87
100
 
88
- piggyback :author, [:name, :birthday, :rating]
101
+ piggyback_attr :name, :birthday, :rating, :from => :user
89
102
 
90
103
 
91
104
  ### Computed values
92
105
 
93
- If you want to use an SQL-expression for selecting an attribute, Piggyback can also help you with that. If `Author` didn't have a single `name` attribute, but `first_name` and `last_name`, you could concatenate them into a single attribute:
106
+ If you want to use an SQL-expression for selecting an attribute, Piggyback can also help you with that. If `User` didn't have a single `name` attribute, but `first_name` and `last_name`, you could concatenate them into a single attribute:
94
107
 
95
108
  class Post < ActiveRecord::Base
96
- belongs_to :author
97
- piggybacks :author, :author_name => "authors.first_name || ' ' || authors.last_name"
109
+ belongs_to :user
110
+ piggybacks :name, :from => :user, :source => :first_name, :sql => "users.first_name || ' ' || users.last_name"
98
111
  end
99
112
 
100
- post.author_name # => "Donald Duck"
113
+ post.user_name # => "Donald Duck"
101
114
 
102
- In fact, every value you pass in as a string will be treated as raw SQL in the SELECT clause.
115
+ The `:source` option in this case is only required for type-casting and was used because `first_name` is part of the SQL expression. If we would be dealing with a numeric column in this case, the `:source` option would be more relevant since it would be used for type casting.
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'rake'
2
2
  require 'echoe'
3
3
 
4
- Echoe.new('piggyback', '0.2.4') do |p|
4
+ Echoe.new('piggyback', '0.3.0b') do |p|
5
5
  p.description = "Piggyback attributes from associated models with ActiveRecord"
6
6
  p.url = "http://github.com/juni0r/piggyback"
7
7
  p.author = "Andreas Korth"
data/lib/piggyback.rb CHANGED
@@ -1,126 +1,145 @@
1
1
  module Piggyback
2
2
  extend ActiveSupport::Concern
3
3
 
4
- class Association #:nodoc:
5
-
6
- attr_reader :mappings
7
-
8
- def initialize(reflection, mappings = {})
9
- @reflection = reflection
10
- @mappings = mappings
11
- end
12
-
13
- def add_mappings(mappings)
14
- @mappings.merge!(mappings)
15
- end
16
-
17
- def table
18
- @table ||= @reflection.active_record.arel_table
19
- end
20
-
21
- def foreign_table
22
- @foreign_table ||= @reflection.klass.arel_table
23
- end
24
-
25
- def select
26
- @mappings.map do |attr_name, mapped_name|
27
- if attr_name == mapped_name
28
- foreign_table[attr_name]
29
- elsif mapped_name.is_a? Arel::Nodes::SqlLiteral
30
- mapped_name.as(attr_name)
31
- else
32
- foreign_table[mapped_name].as(attr_name)
33
- end
34
- end
35
- end
36
-
37
- def joins
38
- if @reflection.belongs_to?
39
- table.join(foreign_table, Arel::Nodes::OuterJoin).on(foreign_table.primary_key.eq(table[@reflection.primary_key_name]))
40
- else
41
- table.join(foreign_table, Arel::Nodes::OuterJoin).on(foreign_table[@reflection.primary_key_name].eq(table.primary_key))
42
- end.join_sql
43
- end
44
-
45
- def has_attribute?(attr_name)
46
- @mappings.has_key? attr_name
47
- end
48
-
49
- def column(attr_name)
50
- @reflection.klass.columns_hash[@mappings[attr_name]]
51
- end
52
-
53
- def serialized_attribute(attr_name)
54
- @reflection.klass.serialized_attributes[@mappings[attr_name]]
55
- end
56
- end
57
-
58
4
  module ClassMethods
59
-
60
- def piggyback_associations
61
- read_inheritable_attribute(:piggyback_associations) || write_inheritable_attribute(:piggyback_associations, {})
62
- end
63
-
64
- def piggyback_association_for_attribute(attr_name)
65
- piggyback_associations.each_value.detect{ |association| association.has_attribute? attr_name }
5
+
6
+ def piggyback_attributes
7
+ read_inheritable_attribute(:piggyback_attributes) || write_inheritable_attribute(:piggyback_attributes, {})
66
8
  end
9
+
10
+ def define_attribute_methods
11
+ piggyback_attributes.values.each do |attribute|
12
+ columns_hash[attribute.name] = attribute.column
13
+ if attribute.serialized_class
14
+ serialized_attributes[attribute.name] = attribute.serialized_class
15
+ end
16
+ end
17
+ super
18
+ end
67
19
 
68
- def piggybacks(association, *attributes)
20
+ def piggyback_attr(*attributes)
69
21
 
70
- attributes.flatten!
71
- mapped_attributes = attributes.extract_options!
72
- reflection = reflect_on_association(association.to_sym)
22
+ options = attributes.extract_options!
23
+ options.assert_valid_keys(:from, :source, :sql)
24
+
25
+ raise ArgumentError, "No attributes specified for piggybacking" if attributes.empty?
73
26
 
74
- raise ArgumentError, "#{name} has no association named '#{association}'" if reflection.nil?
75
- raise ArgumentError, "Piggyback only supports belongs_to and has_one associations" if reflection.collection?
76
- raise ArgumentError, "No attributes specified for piggybacking" if attributes.empty?
77
-
78
- mappings = {}
27
+ if attributes.many? && (options[:source] || options[:sql])
28
+ raise ArgumentError, "Options :source and :sql can only be used with a single attribute"
29
+ end
79
30
 
80
31
  attributes.each do |attr_name|
81
- mappings[attr_name.to_s] = attr_name.to_s
32
+ attribute = Attribute.new(self, attr_name, options)
33
+ piggyback_attributes[attribute.name] = attribute
82
34
  end
35
+ end
36
+
37
+ def piggyback(*attr_names)
38
+ attr_names.flatten!
39
+ options = attr_names.extract_options!
83
40
 
84
- mapped_attributes.each do |attr_name, mapped_name|
85
- mappings[attr_name.to_s] = mapped_name.is_a?(String) ? Arel.sql(mapped_name) : mapped_name.to_s
41
+ if attr_names.empty?
42
+ attr_names = piggyback_attributes.keys
43
+ else
44
+ froms = Array.wrap(options[:from])
45
+ if froms.any?
46
+ piggyback_attributes.each_value do |attribute|
47
+ attr_names << attribute.name if froms.include?(attribute.from)
48
+ end
49
+ end
86
50
  end
87
51
 
88
- association = (piggyback_associations[reflection.name] ||= Association.new(reflection))
89
- association.add_mappings(mappings)
52
+ attr_names.map!(&:to_s).uniq!
90
53
 
91
- # These default procs for +columns_hash+ and +serialized_attributes+
92
- # allow +read_attribute+ to work transparently and consistently with
93
- # attributes from joined models. They rely heavily on the implementation
94
- # of +read_attribute+ and related methods. Since ActiveRecord doesn't
95
- # provide hooks for overriding this behavior, it is the simplest and
96
- # least intrusive way to implement proper type casting for attributes
97
- # while avoiding code duplication.
98
-
99
- columns_hash.default_proc = lambda do |hash, attr_name|
100
- if association = piggyback_association_for_attribute(attr_name)
101
- association.column(attr_name)
54
+ attributes = []
55
+ piggybacks = []
56
+ joins = []
57
+
58
+ attr_names.each do |attr_name|
59
+
60
+ if attr_name == '*'
61
+ attributes.concat(arel_table.columns)
62
+ next
102
63
  end
64
+
65
+ attribute = piggyback_attributes[attr_name]
66
+ if attribute
67
+ piggybacks << attribute.select_sql
68
+ joins << attribute.join_sql
69
+ else
70
+ attribute = arel_table[attr_name]
71
+ raise "Unkown attribute #{attr_name}" if attribute.nil?
72
+ attributes << attribute
73
+ end
74
+ end
75
+
76
+ if !scoped.select_values.any? && attributes.empty?
77
+ attributes << "#{quoted_table_name}.*"
103
78
  end
79
+
80
+ context = select(attributes + piggybacks)
81
+ joins.any? ? context.joins(joins.uniq) : context
82
+ end
83
+ end
104
84
 
105
- serialized_attributes.default_proc = lambda do |hash, attr_name|
106
- if association = piggyback_association_for_attribute(attr_name)
107
- association.serialized_attribute(attr_name)
85
+ class Attribute #:nodoc:
86
+
87
+ attr_reader :owner, :name, :from, :source, :sql, :reflection
88
+
89
+ def initialize(owner, name, options = {})
90
+ @owner = owner
91
+ @name = name.to_s
92
+ @from = options[:from]
93
+ @source = (options[:source] || name).to_s
94
+ @sql = Arel.sql(options[:sql]) if options[:sql]
95
+
96
+ @reflection = @owner.reflect_on_association(from)
97
+
98
+ raise ArgumentError, "#{@owner} has no association #{from.inspect}" if @reflection.nil?
99
+ raise ArgumentError, "Piggyback only supports belongs_to and has_one associations" if @reflection.collection?
100
+ end
101
+
102
+ def select_sql
103
+ unless defined? @select_sql
104
+ @select_sql = if sql
105
+ sql.as(Arel.sql(name))
106
+ elsif source == name
107
+ reflection.klass.arel_table[name]
108
+ else
109
+ reflection.klass.arel_table[source].as(Arel.sql(name))
108
110
  end
109
111
  end
112
+ @select_sql
110
113
  end
111
-
112
- def piggyback_attributes(assoc_name)
113
- piggyback_associations[assoc_name].mappings.keys
114
+
115
+ def join_sql
116
+ unless defined? @join_sql
117
+ join_table = reflection.klass.arel_table
118
+
119
+ join = owner.arel_table.join(join_table, Arel::Nodes::OuterJoin)
120
+ @join_sql = if reflection.belongs_to?
121
+ join.on(owner.arel_table[reflection.primary_key_name].eq(join_table.primary_key))
122
+ else
123
+ join.on(owner.arel_table.primary_key.eq(join_table[reflection.primary_key_name]))
124
+ end.join_sql
125
+ end
126
+ @join_sql
127
+ end
128
+
129
+ def column
130
+ unless defined? @column
131
+ @column = reflection.klass.columns_hash[source]
132
+ end
133
+ @column
114
134
  end
115
135
 
116
- def piggyback(*assocs)
117
- context = scoped
118
- context = context.select("#{quoted_table_name}.*") unless context.select_values.any?
119
- piggyback_associations.values_at(*assocs).inject(context) do |ctx, assoc|
120
- ctx.select(assoc.select).joins(assoc.joins)
136
+ def serialized_class
137
+ unless defined? @serialized_class
138
+ @serialized_class = reflection.klass.serialized_attributes[source]
121
139
  end
140
+ @serialized_class
122
141
  end
123
- end
142
+ end
124
143
  end
125
144
 
126
145
  ActiveRecord::Base.send(:include, Piggyback)
data/piggyback.gemspec CHANGED
@@ -2,20 +2,20 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{piggyback}
5
- s.version = "0.2.4"
5
+ s.version = "0.3.0b"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
- s.authors = ["Andreas Korth"]
9
- s.date = %q{2011-03-24}
8
+ s.authors = [%q{Andreas Korth}]
9
+ s.date = %q{2011-08-15}
10
10
  s.description = %q{Piggyback attributes from associated models with ActiveRecord}
11
11
  s.email = %q{andreas.korth@gmail.com}
12
- s.extra_rdoc_files = ["CHANGELOG", "README.markdown", "lib/piggyback.rb"]
13
- s.files = ["CHANGELOG", "MIT-LICENSE", "README.markdown", "Rakefile", "lib/piggyback.rb", "piggyback.gemspec", "spec/models.rb", "spec/piggyback_spec.rb", "spec/spec_helper.rb", "Manifest"]
12
+ s.extra_rdoc_files = [%q{CHANGELOG}, %q{README.markdown}, %q{lib/piggyback.rb}]
13
+ s.files = [%q{CHANGELOG}, %q{MIT-LICENSE}, %q{Manifest}, %q{README.markdown}, %q{Rakefile}, %q{lib/piggyback.rb}, %q{piggyback.tmproj}, %q{spec/models.rb}, %q{spec/piggyback_spec.rb}, %q{spec/spec_helper.rb}, %q{piggyback.gemspec}]
14
14
  s.homepage = %q{http://github.com/juni0r/piggyback}
15
- s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Piggyback", "--main", "README.markdown"]
16
- s.require_paths = ["lib"]
15
+ s.rdoc_options = [%q{--line-numbers}, %q{--inline-source}, %q{--title}, %q{Piggyback}, %q{--main}, %q{README.markdown}]
16
+ s.require_paths = [%q{lib}]
17
17
  s.rubyforge_project = %q{piggyback}
18
- s.rubygems_version = %q{1.5.2}
18
+ s.rubygems_version = %q{1.8.8}
19
19
  s.summary = %q{Piggyback attributes from associated models with ActiveRecord}
20
20
 
21
21
  if s.respond_to? :specification_version then
data/piggyback.tmproj ADDED
@@ -0,0 +1,169 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>currentDocument</key>
6
+ <string>lib/piggyback.rb</string>
7
+ <key>documents</key>
8
+ <array>
9
+ <dict>
10
+ <key>expanded</key>
11
+ <true/>
12
+ <key>name</key>
13
+ <string>piggyback</string>
14
+ <key>regexFileFilter</key>
15
+ <string>!.*\.(lock|rvmrc|tmproj|gemspec)|(Manifest)</string>
16
+ <key>regexFolderFilter</key>
17
+ <string>!.*/(\.[^/]*|tmp|pkg|log|doc)$</string>
18
+ <key>sourceDirectory</key>
19
+ <string></string>
20
+ </dict>
21
+ </array>
22
+ <key>fileHierarchyDrawerWidth</key>
23
+ <integer>202</integer>
24
+ <key>metaData</key>
25
+ <dict>
26
+ <key>README.markdown</key>
27
+ <dict>
28
+ <key>caret</key>
29
+ <dict>
30
+ <key>column</key>
31
+ <integer>288</integer>
32
+ <key>line</key>
33
+ <integer>77</integer>
34
+ </dict>
35
+ <key>columnSelection</key>
36
+ <false/>
37
+ <key>firstVisibleColumn</key>
38
+ <integer>152</integer>
39
+ <key>firstVisibleLine</key>
40
+ <integer>54</integer>
41
+ <key>selectFrom</key>
42
+ <dict>
43
+ <key>column</key>
44
+ <integer>298</integer>
45
+ <key>line</key>
46
+ <integer>77</integer>
47
+ </dict>
48
+ <key>selectTo</key>
49
+ <dict>
50
+ <key>column</key>
51
+ <integer>288</integer>
52
+ <key>line</key>
53
+ <integer>77</integer>
54
+ </dict>
55
+ </dict>
56
+ <key>lib/piggyback.rb</key>
57
+ <dict>
58
+ <key>caret</key>
59
+ <dict>
60
+ <key>column</key>
61
+ <integer>16</integer>
62
+ <key>line</key>
63
+ <integer>96</integer>
64
+ </dict>
65
+ <key>firstVisibleColumn</key>
66
+ <integer>0</integer>
67
+ <key>firstVisibleLine</key>
68
+ <integer>72</integer>
69
+ </dict>
70
+ <key>spec/models.rb</key>
71
+ <dict>
72
+ <key>caret</key>
73
+ <dict>
74
+ <key>column</key>
75
+ <integer>44</integer>
76
+ <key>line</key>
77
+ <integer>1</integer>
78
+ </dict>
79
+ <key>columnSelection</key>
80
+ <false/>
81
+ <key>firstVisibleColumn</key>
82
+ <integer>0</integer>
83
+ <key>firstVisibleLine</key>
84
+ <integer>0</integer>
85
+ <key>selectFrom</key>
86
+ <dict>
87
+ <key>column</key>
88
+ <integer>29</integer>
89
+ <key>line</key>
90
+ <integer>1</integer>
91
+ </dict>
92
+ <key>selectTo</key>
93
+ <dict>
94
+ <key>column</key>
95
+ <integer>44</integer>
96
+ <key>line</key>
97
+ <integer>1</integer>
98
+ </dict>
99
+ </dict>
100
+ <key>spec/piggyback_spec.rb</key>
101
+ <dict>
102
+ <key>caret</key>
103
+ <dict>
104
+ <key>column</key>
105
+ <integer>0</integer>
106
+ <key>line</key>
107
+ <integer>0</integer>
108
+ </dict>
109
+ <key>firstVisibleColumn</key>
110
+ <integer>0</integer>
111
+ <key>firstVisibleLine</key>
112
+ <integer>0</integer>
113
+ </dict>
114
+ <key>spec/spec_helper.rb</key>
115
+ <dict>
116
+ <key>caret</key>
117
+ <dict>
118
+ <key>column</key>
119
+ <integer>19</integer>
120
+ <key>line</key>
121
+ <integer>4</integer>
122
+ </dict>
123
+ <key>firstVisibleColumn</key>
124
+ <integer>0</integer>
125
+ <key>firstVisibleLine</key>
126
+ <integer>0</integer>
127
+ </dict>
128
+ </dict>
129
+ <key>openDocuments</key>
130
+ <array>
131
+ <string>spec/piggyback_spec.rb</string>
132
+ <string>README.markdown</string>
133
+ <string>spec/models.rb</string>
134
+ <string>spec/spec_helper.rb</string>
135
+ <string>lib/piggyback.rb</string>
136
+ </array>
137
+ <key>showFileHierarchyDrawer</key>
138
+ <false/>
139
+ <key>showFileHierarchyPanel</key>
140
+ <true/>
141
+ <key>treeState</key>
142
+ <dict>
143
+ <key>piggyback</key>
144
+ <dict>
145
+ <key>isExpanded</key>
146
+ <true/>
147
+ <key>subItems</key>
148
+ <dict>
149
+ <key>lib</key>
150
+ <dict>
151
+ <key>isExpanded</key>
152
+ <true/>
153
+ <key>subItems</key>
154
+ <dict/>
155
+ </dict>
156
+ <key>spec</key>
157
+ <dict>
158
+ <key>isExpanded</key>
159
+ <true/>
160
+ <key>subItems</key>
161
+ <dict/>
162
+ </dict>
163
+ </dict>
164
+ </dict>
165
+ </dict>
166
+ <key>windowFrame</key>
167
+ <string>{{93, 131}, {1568, 1024}}</string>
168
+ </dict>
169
+ </plist>
data/spec/models.rb CHANGED
@@ -26,9 +26,14 @@ class Essential < ActiveRecord::Base
26
26
  has_one :detail, :inverse_of => :essential
27
27
  has_many :details
28
28
 
29
- piggybacks :detail, [:first_name, :last_name, :count, :checked, :birthday, :baggage],
30
- :name => "details.first_name || ' ' || details.last_name",
31
- :detail_updated_at => :updated_at
29
+ piggyback_attr :first_name,
30
+ :last_name,
31
+ :count,
32
+ :checked,
33
+ :birthday,
34
+ :baggage, from: :detail
35
+ piggyback_attr :detail_updated_at, from: :detail, source: :updated_at
36
+ piggyback_attr :name, from: :detail, source: :first_name, sql:"details.first_name || ' ' || details.last_name"
32
37
  end
33
38
 
34
39
  class Detail < ActiveRecord::Base
@@ -36,7 +41,7 @@ class Detail < ActiveRecord::Base
36
41
 
37
42
  serialize :baggage
38
43
 
39
- piggybacks :essential, :token
44
+ piggyback_attr :token, from: :essential
40
45
 
41
46
  def name
42
47
  "#{first_name} #{last_name}"
@@ -9,26 +9,30 @@ describe Piggyback do
9
9
  end
10
10
 
11
11
  it "should require the association to be defined" do
12
- definition_should_raise "Essential has no association named 'bogus'" do
13
- piggybacks :bogus, [:attribute]
12
+ definition_should_raise "Essential has no association :bogus" do
13
+ piggyback_attr :attribute, from: :bogus
14
14
  end
15
15
  end
16
16
 
17
17
  it "should not work with has_many associations" do
18
18
  definition_should_raise "Piggyback only supports belongs_to and has_one associations" do
19
- piggybacks :details, [:attribute]
19
+ piggyback_attr :attribute, from: :details
20
20
  end
21
21
  end
22
22
 
23
23
  it "should require attributes" do
24
24
  definition_should_raise "No attributes specified for piggybacking" do
25
- piggybacks :detail
25
+ piggyback_attr from: :detail
26
26
  end
27
27
  end
28
28
  end
29
-
29
+
30
30
  context "relation" do
31
31
 
32
+ ESSENTIAL_ATTRIBUTES = %w{ id token created_at updated_at }
33
+ DETAIL_ATTRIBUTES = %w{ first_name last_name name count checked birthday baggage detail_updated_at }
34
+ ALL_ATTRIBUTES = ESSENTIAL_ATTRIBUTES + DETAIL_ATTRIBUTES
35
+
32
36
  before do
33
37
  Essential.create!(:token => "ESSENTIAL").create_detail(
34
38
  :first_name => "John",
@@ -39,70 +43,90 @@ describe Piggyback do
39
43
  :baggage => {:key => "value"})
40
44
  end
41
45
 
42
- let :essential do
43
- Essential.piggyback(:detail).first
44
- end
45
-
46
- it "should include records without an association" do
47
- Essential.create!(:token => "WITHOUT_DETAIL")
48
- Essential.piggyback(:detail).should have(2).items
46
+ it "should work with belongs_to associations" do
47
+ Detail.piggyback(:token).first.token.should eql("ESSENTIAL")
48
+ Detail.create!
49
+ Detail.piggyback(:token).should have(2).items
49
50
  end
50
51
 
51
- it "should not load the association" do
52
- essential.should_not_receive(:detail)
53
- essential.first_name.should be_present
52
+ it "should include records without an association" do
53
+ Essential.create!
54
+ Essential.piggyback(:name).should have(2).items
54
55
  end
55
-
56
- it "should read string attributes" do
57
- essential.first_name.should eql("John")
56
+
57
+ it "should include all native and piggybacked attributes if none are specified" do
58
+ essential = Essential.piggyback.first
59
+ essential.attributes.keys.should =~ ALL_ATTRIBUTES
58
60
  end
59
61
 
60
- it "should read integer attributes" do
61
- essential.count.should eql(23)
62
+ it "should include all native attributes when none were explicitly specified" do
63
+ essential = Essential.piggyback(:checked).first
64
+ essential.attributes.keys.should =~ ESSENTIAL_ATTRIBUTES + %w{ checked }
62
65
  end
63
66
 
64
- it "should read boolean attributes" do
65
- essential.checked.should eql(false)
67
+ it "should include only native attributes that were explicitly specified" do
68
+ essential = Essential.piggyback(:id, :name).first
69
+ essential.attributes.keys.should =~ %w{ id name }
66
70
  end
67
71
 
68
- it "should read date attributes" do
69
- essential.birthday.should eql(Date.new(1974,12,18))
72
+ it "should include all native attributes when using the wildcard :*" do
73
+ essential = Essential.piggyback(:*).first
74
+ essential.attributes.keys.should =~ ESSENTIAL_ATTRIBUTES
70
75
  end
71
76
 
72
- it "should read serialized attributes" do
73
- essential.baggage.should eql({:key => "value"})
77
+ it "should include all attributes from the specified associations" do
78
+ essential = Essential.piggyback(from: :detail).first
79
+ essential.attributes.keys.should =~ ALL_ATTRIBUTES
74
80
  end
75
-
76
- it "should read a mapped attribute" do
77
- essential.detail_updated_at.should be_instance_of(Time)
81
+
82
+ it "should include all native attributes and those of the specified associations" do
83
+ essential = Essential.piggyback(:*, from: :detail).first
84
+ essential.attributes.keys.should =~ ALL_ATTRIBUTES
78
85
  end
79
86
 
80
- it "should read computed attributes" do
81
- essential.name.should eql("John Doe")
87
+ it "should include native and piggybackd attributes as specified" do
88
+ essential = Essential.piggyback(:id, :token, :first_name, :last_name).first
89
+ essential.attributes.keys.should =~ %w{ id token first_name last_name }
82
90
  end
83
91
 
84
- it "should work with belongs_to associations" do
85
- Detail.piggyback(:essential).first.token.should eql("ESSENTIAL")
86
- Detail.create!
87
- Detail.piggyback(:essential).should have(2).items
92
+ it "should not include native columns if any were selected before" do
93
+ essential = Essential.select('essentials.id').piggyback(:checked).first
94
+ essential.attributes.keys.should =~ %w{ id checked }
88
95
  end
89
- end
90
-
91
- context "scope" do
92
96
 
93
- it "should SELECT * if no columns were selected before" do
94
- Essential.piggyback(:detail).to_sql.should match(/^SELECT "essentials"\.\*,/)
95
- end
97
+ context "attribute methods" do
98
+
99
+ let :essential do
100
+ Essential.piggyback.first
101
+ end
102
+
103
+ it "should read string attributes" do
104
+ essential.first_name.should eql("John")
105
+ end
106
+
107
+ it "should read integer attributes" do
108
+ essential.count.should eql(23)
109
+ end
110
+
111
+ it "should read boolean attributes" do
112
+ essential.checked.should eql(false)
113
+ end
114
+
115
+ it "should read date attributes" do
116
+ essential.birthday.should eql(Date.new(1974,12,18))
117
+ end
118
+
119
+ it "should read serialized attributes" do
120
+ essential.baggage.should eql({:key => "value"})
121
+ end
122
+
123
+ it "should read a mapped attribute" do
124
+ essential.detail_updated_at.should be_instance_of(Time)
125
+ end
96
126
 
97
- it "should not SELECT * if columns were selected before" do
98
- sql = Essential.select(:id).piggyback(:detail).to_sql
99
- sql.should match(/^SELECT id,/)
100
- sql.should_not match(/"essentials"\.\*/)
127
+ it "should read computed attributes" do
128
+ essential.name.should eql("John Doe")
129
+ end
101
130
  end
102
- end
103
-
104
- it "should return the column names for a given association name" do
105
- Essential.piggyback_attributes(:detail).should =~
106
- %w{ first_name last_name name count checked birthday baggage detail_updated_at }
107
- end
131
+ end
108
132
  end
data/spec/spec_helper.rb CHANGED
@@ -12,5 +12,6 @@ end
12
12
 
13
13
  ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:", :verbosity => "silent")
14
14
  ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.join(GEM_ROOT,'log','test.log'))
15
+ ActiveRecord::Base::include_root_in_json = false
15
16
 
16
17
  require 'models'
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: piggyback
3
3
  version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 0.2.4
4
+ prerelease: 5
5
+ version: 0.3.0b
6
6
  platform: ruby
7
7
  authors:
8
8
  - Andreas Korth
@@ -10,8 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-03-24 00:00:00 +01:00
14
- default_executable:
13
+ date: 2011-08-15 00:00:00 Z
15
14
  dependencies:
16
15
  - !ruby/object:Gem::Dependency
17
16
  name: activerecord
@@ -59,15 +58,15 @@ extra_rdoc_files:
59
58
  files:
60
59
  - CHANGELOG
61
60
  - MIT-LICENSE
61
+ - Manifest
62
62
  - README.markdown
63
63
  - Rakefile
64
64
  - lib/piggyback.rb
65
- - piggyback.gemspec
65
+ - piggyback.tmproj
66
66
  - spec/models.rb
67
67
  - spec/piggyback_spec.rb
68
68
  - spec/spec_helper.rb
69
- - Manifest
70
- has_rdoc: true
69
+ - piggyback.gemspec
71
70
  homepage: http://github.com/juni0r/piggyback
72
71
  licenses: []
73
72
 
@@ -96,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
95
  requirements: []
97
96
 
98
97
  rubyforge_project: piggyback
99
- rubygems_version: 1.5.2
98
+ rubygems_version: 1.8.8
100
99
  signing_key:
101
100
  specification_version: 3
102
101
  summary: Piggyback attributes from associated models with ActiveRecord