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 +4 -0
- data/Manifest +2 -2
- data/README.markdown +44 -31
- data/Rakefile +1 -1
- data/lib/piggyback.rb +117 -98
- data/piggyback.gemspec +8 -8
- data/piggyback.tmproj +169 -0
- data/spec/models.rb +9 -4
- data/spec/piggyback_spec.rb +75 -51
- data/spec/spec_helper.rb +1 -0
- metadata +7 -8
data/CHANGELOG
CHANGED
data/Manifest
CHANGED
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
|
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
|
11
|
+
class User < ActiveRecord::Base
|
12
12
|
has_many :posts
|
13
13
|
end
|
14
14
|
|
15
15
|
class Post < ActiveRecord::Base
|
16
|
-
belongs_to :
|
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.*,
|
22
|
-
.joins("JOIN
|
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
|
26
|
-
post.
|
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 `
|
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.*,
|
33
|
-
.joins("JOIN
|
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.
|
37
|
-
post.
|
36
|
+
post.user_birthday # => "2011-03-01"
|
37
|
+
post.user_rating # => "4.5"
|
38
38
|
|
39
|
-
Any attributes originating from the `
|
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 :
|
44
|
+
belongs_to :user
|
45
45
|
|
46
|
-
def
|
47
|
-
Date.parse(read_attribute(:
|
46
|
+
def user_birthday
|
47
|
+
Date.parse(read_attribute(:user_birthday))
|
48
48
|
end
|
49
49
|
|
50
|
-
def
|
51
|
-
read_attribute(:
|
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 :
|
66
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
post.
|
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.
|
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
|
-
|
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 `
|
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 :
|
97
|
-
piggybacks :
|
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.
|
113
|
+
post.user_name # => "Donald Duck"
|
101
114
|
|
102
|
-
|
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.
|
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
|
61
|
-
read_inheritable_attribute(:
|
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
|
20
|
+
def piggyback_attr(*attributes)
|
69
21
|
|
70
|
-
attributes.
|
71
|
-
|
72
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
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
|
-
|
85
|
-
|
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
|
-
|
89
|
-
association.add_mappings(mappings)
|
52
|
+
attr_names.map!(&:to_s).uniq!
|
90
53
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
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
|
113
|
-
|
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
|
117
|
-
|
118
|
-
|
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.
|
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 = [
|
9
|
-
s.date = %q{2011-
|
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 = [
|
13
|
-
s.files = [
|
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 = [
|
16
|
-
s.require_paths = [
|
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.
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
44
|
+
piggyback_attr :token, from: :essential
|
40
45
|
|
41
46
|
def name
|
42
47
|
"#{first_name} #{last_name}"
|
data/spec/piggyback_spec.rb
CHANGED
@@ -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
|
13
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
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
|
52
|
-
|
53
|
-
|
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
|
57
|
-
essential.
|
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
|
61
|
-
essential.
|
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
|
65
|
-
essential.
|
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
|
69
|
-
essential
|
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
|
73
|
-
essential.
|
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
|
77
|
-
essential.
|
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
|
81
|
-
essential.
|
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
|
85
|
-
|
86
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
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.
|
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-
|
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.
|
65
|
+
- piggyback.tmproj
|
66
66
|
- spec/models.rb
|
67
67
|
- spec/piggyback_spec.rb
|
68
68
|
- spec/spec_helper.rb
|
69
|
-
-
|
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.
|
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
|