attr_encodable 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Flip Sasser
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,143 @@
1
+ attr_encodable
2
+ =
3
+
4
+ Never override `as_json` again! **attr_encodable** adds attribute black- or white-listing for ActiveRecord serialization, as well as default serialization options. This is especially useful for protecting private attributes when building a public API.
5
+
6
+ Install
7
+ ==
8
+
9
+ Install using Rubygems:
10
+
11
+ gem install attr_encodable
12
+
13
+ Install using Bundler:
14
+
15
+ gem 'attr_encodable'
16
+
17
+ Install in Rails 2.x (in your environment.rb file)
18
+
19
+ config.gem 'attr_encodable'
20
+
21
+ Usage
22
+ ==
23
+
24
+ White-listing
25
+ ===
26
+
27
+ You can whitelist or blacklist attributes for serialization using the `attr_encodable` and `attr_unencodable` class methods. Let's look at an example. For this example, we'll use the following classes:
28
+
29
+ class User < ActiveRecord::Base
30
+ has_many :permissions
31
+ validates_presence_of :email, :password
32
+
33
+ def foobar
34
+ "baz"
35
+ end
36
+ end
37
+
38
+ class Permission < ActiveRecord::Base
39
+ belongs_to :user
40
+ validates_presence_of :name, :user
41
+
42
+ def hello
43
+ "World!"
44
+ end
45
+ end
46
+
47
+ ... with the following schema:
48
+
49
+ create_table :permissions, :force => true do |t|
50
+ t.belongs_to :user
51
+ t.string :name
52
+ end
53
+
54
+ create_table :users, :force => true do |t|
55
+ t.string :login, :limit => 48
56
+ t.string :email, :limit => 128
57
+ t.string :name, :limit => 32
58
+ t.string :password, :limit => 60
59
+ t.boolean :admin, :default => false
60
+ end
61
+
62
+ Let's make a user and try encoding them:
63
+
64
+ @user = User.create(:name => "Flip", :email => "flip@x451.com", :password => "awesomesauce", :admin => true)
65
+ => #<User id: 1, login: nil, email: "flip@x451.com", name: "Flip", password: "awesomesauce", admin: true>
66
+ @user.to_json
67
+ => {"name":"Flip","admin":true,"id":1,"password":"awesomesauce","login":null,"email":"flip@x451.com"}
68
+
69
+ Trouble is, we don't want their admin status OR their password coming through in our API. So why not protect their information a little bit?
70
+
71
+ User.attr_encodable :id, :name, :login, :email
72
+ @user.to_json
73
+ => {"name":"Flip","id":1,"login":null,"email":"flip@x451.com"}
74
+
75
+ Ah, that's so much better! Now whenever we encode a user instance we'll be showing only some default information.
76
+
77
+ `attr_unencodable` is similar, except that it bans an attribute. Following along with the example above, if we then called `attr_unencodable`, we could
78
+ restrict our user's information even more. Let's say I don't want my e-mail getting out:
79
+
80
+ User.attr_unencodable :email
81
+ @user.to_json
82
+ => {"name":"Flip","id":1,"login":null}
83
+
84
+ Alright! Now you can't see my e-mail. Sucker.
85
+
86
+ Default `:include` and `:method` options
87
+ ===
88
+
89
+ `to_json` isn't just concerned with attributes. It also supports `:include`, which includes a relationship with `to_json` called on **it**, as well `:methods`, which adds the result of calling methods on the instance as well.
90
+
91
+ Let's try it out.
92
+
93
+ User.attr_encodable :foobar
94
+ @user.to_json
95
+ => {"name":"Flip","foobar":"baz","id":1,"login":null}
96
+
97
+ With includes, our example might look like this:
98
+
99
+ class User < ActiveRecord::Base
100
+ attr_encodable :id, :name, :login, :permissions
101
+ has_many :permissions
102
+ end
103
+
104
+ @user.to_json
105
+ => {"name":"Flip","foobar":"baz","id":1,"login":null,"permissions":[]}
106
+
107
+ Neato! And of course, when `:permissions` is serialized, it will take into account any `attr_encodable` settings the Permissions class has!
108
+
109
+ Renaming Attributes
110
+ ===
111
+
112
+ Sometimes you don't want an attribute to come out in JSON named what it's named in the database. There are two options you can pursue here.
113
+
114
+ Prefix it!
115
+ ====
116
+
117
+ **attr_encodable** supports prefixing of attribute names. Just pass an options hash onto the end of the method with a :prefix key and you're good to go. Example:
118
+
119
+ class User < ActiveRecord::Base
120
+ attr_encodable :ed, :prefix => :i_will_hunt
121
+ end
122
+
123
+ @user.to_json
124
+ => {"i_will_hunt_ed":true}
125
+
126
+ Rename it completely!
127
+ ====
128
+
129
+ If you don't want to prefix, just rename the whole damn thing:
130
+
131
+ class User < ActiveRecord::Base
132
+ attr_encodable :admin => :superuser
133
+ end
134
+
135
+ @user.to_json
136
+ #=> {"superuser":true}
137
+
138
+ Renaming and prefixing work for any `:include` and `:methods` arguments you pass in as well!
139
+
140
+ Okay, that's all. Thanks for stopping by.
141
+
142
+ Copyright &copy; 2011 Flip Sasser
143
+
@@ -0,0 +1,132 @@
1
+ require 'active_record'
2
+
3
+ module Encodable
4
+ module ClassMethods
5
+ def attr_encodable(*attributes)
6
+ prefix = begin
7
+ if attributes.last.is_a?(Hash)
8
+ attributes.last.assert_valid_keys(:prefix)
9
+ prefix = attributes.extract_options![:prefix]
10
+ end
11
+ rescue ArgumentError
12
+ end
13
+ unless @encodable_whitelist_started
14
+ # Since we're white-listing, make sure we black-list every attribute to begin with
15
+ unencodable_attributes.push *column_names.map(&:to_sym)
16
+ @encodable_whitelist_started = true
17
+ end
18
+ stash_encodable_attribute = lambda {|method, value|
19
+ if prefix
20
+ value = "#{prefix}_#{value}"
21
+ end
22
+ method = method.to_sym
23
+ value = value.to_sym
24
+ renamed_encoded_attributes.merge!({method => value}) if method != value
25
+ # Un-black-list any attribute we white-listed
26
+ unencodable_attributes.delete method
27
+ default_attributes.push method
28
+ }
29
+ attributes.each do |attribute|
30
+ if attribute.is_a?(Hash)
31
+ attribute.each do |method, value|
32
+ stash_encodable_attribute.call(method, value)
33
+ end
34
+ else
35
+ stash_encodable_attribute.call(attribute, attribute)
36
+ end
37
+ end
38
+ end
39
+
40
+ def attr_unencodable(*attributes)
41
+ unencodable_attributes.push *attributes.map(&:to_sym)
42
+ end
43
+
44
+ def default_attributes
45
+ @default_attributes ||= begin
46
+ default_attributes = []
47
+ superk = superclass
48
+ while superk.respond_to?(:default_attributes)
49
+ default_attributes.push(*superk.default_attributes)
50
+ superk = superk.superclass
51
+ end
52
+ default_attributes
53
+ end
54
+ end
55
+
56
+ def renamed_encoded_attributes
57
+ @renamed_encoded_attributes ||= begin
58
+ renamed_encoded_attributes = {}
59
+ superk = superclass
60
+ while superk.respond_to?(:renamed_encoded_attributes)
61
+ renamed_encoded_attributes.merge!(superk.renamed_encoded_attributes)
62
+ superk = superk.superclass
63
+ end
64
+ renamed_encoded_attributes
65
+ end
66
+ end
67
+
68
+ def unencodable_attributes
69
+ @unencodable_attributes ||= begin
70
+ unencodable_attributes = []
71
+ superk = superclass
72
+ while superk.respond_to?(:unencodable_attributes)
73
+ unencodable_attributes.push(*superk.unencodable_attributes)
74
+ superk = superk.superclass
75
+ end
76
+ unencodable_attributes
77
+ end
78
+ end
79
+ end
80
+
81
+ module InstanceMethods
82
+ def serializable_hash(options = {})
83
+ if options && options[:only]
84
+ # We DON'T want to fuck with :only and :except showing up in the same call. This is a disaster.
85
+ super
86
+ else
87
+ options ||= {}
88
+ original_except = if options[:except]
89
+ options[:except] = Array(options[:except]).map(&:to_sym)
90
+ else
91
+ options[:except] = []
92
+ end
93
+ # This is a little bit confusing. ActiveRecord's default behavior is to apply the :except arguments you pass
94
+ # in to any :include options UNLESS it's overridden on the :include option. In the event that we have some
95
+ # *default* excepts that come from Encodable, we want to ignore those and pass only whatever the original
96
+ # :except options from the user were on down to the :include guys.
97
+ inherited_except = original_except - self.class.default_attributes
98
+ case options[:include]
99
+ when Array, Symbol
100
+ # Convert includes arrays or singleton symbols into a hash with our original_except scope
101
+ includes = Array(options[:include])
102
+ options[:include] = Hash[*includes.map{|association| [association, {:except => inherited_except}]}.flatten]
103
+ else
104
+ options[:include] ||= {}
105
+ end
106
+ # Exclude the black-list
107
+ options[:except].push *self.class.unencodable_attributes
108
+ # Include any default :include or :methods arguments that were passed in earlier
109
+ self.class.default_attributes.each do |attribute, as|
110
+ if association = self.class.reflect_on_association(attribute)
111
+ options[:include][attribute] = {:except => inherited_except}
112
+ elsif respond_to?(attribute) && !self.class.column_names.include?(attribute.to_s)
113
+ options[:methods] ||= Array(options[:methods]).compact
114
+ options[:methods].push attribute
115
+ end
116
+ end
117
+ as_json = super(options)
118
+ unless self.class.renamed_encoded_attributes.empty?
119
+ self.class.renamed_encoded_attributes.each do |attribute, as|
120
+ as_json[as.to_s] = as_json.delete(attribute) || as_json.delete(attribute.to_s)
121
+ end
122
+ end
123
+ as_json
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ if defined? ActiveRecord::Base
130
+ ActiveRecord::Base.extend Encodable::ClassMethods
131
+ ActiveRecord::Base.send(:include, Encodable::InstanceMethods)
132
+ end
@@ -0,0 +1,193 @@
1
+ require "lib/attr_encodable"
2
+
3
+ describe Encodable do
4
+ it "should automatically extend ActiveRecord::Base" do
5
+ ActiveRecord::Base.should respond_to(:attr_encodable)
6
+ ActiveRecord::Base.should respond_to(:attr_unencodable)
7
+ end
8
+
9
+ before :each do
10
+ ActiveRecord::Base.include_root_in_json = false
11
+ ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:', :pool => 5, :timeout => 5000})
12
+ class ::Permission < ActiveRecord::Base; belongs_to :user; def hello; "World!"; end; end
13
+ class ::User < ActiveRecord::Base; has_many :permissions; def foobar; "baz"; end; end
14
+ silence_stream(STDOUT) do
15
+ ActiveRecord::Schema.define do
16
+ create_table :permissions, :force => true do |t|
17
+ t.belongs_to :user
18
+ t.string :name
19
+ end
20
+ create_table :users, :force => true do |t|
21
+ t.string "login", :limit => 48
22
+ t.string "email", :limit => 128
23
+ t.string "first_name", :limit => 32
24
+ t.string "last_name", :limit => 32
25
+ t.string "encrypted_password", :limit => 60
26
+ t.boolean "developer", :default => false
27
+ t.boolean "admin", :default => false
28
+ t.boolean "password_set", :default => true
29
+ t.boolean "verified", :default => false
30
+ t.datetime "created_at"
31
+ t.datetime "updated_at"
32
+ t.integer "notifications"
33
+ end
34
+ end
35
+ end
36
+ @user = User.create({
37
+ :login => "flipsasser",
38
+ :first_name => "flip",
39
+ :last_name => "sasser",
40
+ :email => "flip@foobar.com",
41
+ :encrypted_password => ActiveSupport::SecureRandom.hex(30),
42
+ :developer => true,
43
+ :admin => true,
44
+ :password_set => true,
45
+ :verified => true,
46
+ :notifications => 7
47
+ })
48
+ @user.permissions.create(:name => "create_blog_posts")
49
+ @user.permissions.create(:name => "edit_blog_posts")
50
+ # Reset the options for each test
51
+ [Permission, User].each do |klass|
52
+
53
+ klass.class_eval do
54
+ @default_attributes = nil
55
+ @encodable_whitelist_started = nil
56
+ @renamed_encoded_attributes = nil
57
+ @unencodable_attributes = nil
58
+ end
59
+ end
60
+ end
61
+
62
+ it "should favor whitelisting to blacklisting" do
63
+ User.unencodable_attributes.should == []
64
+ User.attr_unencodable 'foo', 'bar', 'baz'
65
+ User.unencodable_attributes.should == [:foo, :bar, :baz]
66
+ User.attr_encodable :id, :first_name
67
+ User.unencodable_attributes.map(&:to_s).should == ['foo', 'bar', 'baz'] + User.column_names - ['id', 'first_name']
68
+ end
69
+
70
+ describe "at the parent model level" do
71
+ it "should not mess with to_json unless when attr_encodable and attr_unencodable are not set" do
72
+ @user.as_json == @user.attributes
73
+ end
74
+
75
+ it "should not mess with :include options" do
76
+ @user.as_json(:include => :permissions) == @user.attributes.merge(:permissions => @user.permissions.as_json)
77
+ end
78
+
79
+ it "should not mess with :methods options" do
80
+ @user.as_json(:methods => :foobar) == @user.attributes.merge(:foobar => "baz")
81
+ end
82
+
83
+ it "should allow me to whitelist attributes" do
84
+ User.attr_encodable :login, :first_name, :last_name
85
+ @user.as_json.should == @user.attributes.slice('login', 'first_name', 'last_name')
86
+ end
87
+
88
+ it "should allow me to blacklist attributes" do
89
+ User.attr_unencodable :login, :first_name, :last_name
90
+ @user.as_json.should == @user.attributes.except('login', 'first_name', 'last_name')
91
+ end
92
+
93
+ # Of note is the INSANITY of ActiveRecord in that it applies :only / :except to :include as well. Which is
94
+ # obviously insane. Similarly, it doesn't allow :methods to come along when :only is specified. Good god, what
95
+ # a shame.
96
+ it "should allow me to whitelist attributes without messing with :include" do
97
+ User.attr_encodable :login, :first_name, :last_name
98
+ @user.as_json(:include => :permissions).should == @user.attributes.slice('login', 'first_name', 'last_name').merge(:permissions => @user.permissions.as_json)
99
+ end
100
+
101
+ it "should allow me to blacklist attributes without messing with :include and :methods" do
102
+ User.attr_unencodable :login, :first_name, :last_name
103
+ @user.as_json(:include => :permissions, :methods => :foobar).should == @user.attributes.except('login', 'first_name', 'last_name').merge(:permissions => @user.permissions.as_json, :foobar => "baz")
104
+ end
105
+
106
+ it "should not screw with :include if it's a hash" do
107
+ User.attr_unencodable :login, :first_name, :last_name
108
+ @user.as_json(:include => {:permissions => {:methods => :hello, :except => :id}}, :methods => :foobar).should == @user.attributes.except('login', 'first_name', 'last_name').merge(:permissions => @user.permissions.as_json(:methods => :hello, :except => :id), :foobar => "baz")
109
+ end
110
+ end
111
+
112
+ describe "at the child model level when the paren model has attr_encodable set" do
113
+ before :each do
114
+ User.attr_encodable :login, :first_name, :last_name
115
+ end
116
+
117
+ it "should not mess with to_json unless when attr_encodable and attr_unencodable are not set on the child, but are on the parent" do
118
+ @user.permissions.as_json == @user.permissions.map(&:attributes)
119
+ end
120
+
121
+ it "should not mess with :include options" do
122
+ # This is testing that the implicit ban on the :id attribute from User.attr_encodable is not
123
+ # applying to serialization of permissions
124
+ @user.as_json(:include => :permissions)[:permissions].first['id'].should_not be_nil
125
+ end
126
+
127
+ it "should inherit any attr_encodable options from the child model" do
128
+ User.attr_encodable :id
129
+ Permission.attr_encodable :name
130
+ as_json = @user.as_json(:include => :permissions)
131
+ as_json[:permissions].first['id'].should be_nil
132
+ as_json['id'].should_not be_nil
133
+ end
134
+
135
+ # it "should allow me to whitelist attributes" do
136
+ # User.attr_encodable :login, :first_name, :last_name
137
+ # @user.as_json.should == @user.attributes.slice('login', 'first_name', 'last_name')
138
+ # end
139
+ #
140
+ # it "should allow me to blacklist attributes" do
141
+ # User.attr_unencodable :login, :first_name, :last_name
142
+ # @user.as_json.should == @user.attributes.except('login', 'first_name', 'last_name')
143
+ # end
144
+ end
145
+
146
+ it "should let me specify automatic includes as well as attributes" do
147
+ User.attr_encodable :login, :first_name, :id, :permissions
148
+ @user.as_json.should == @user.attributes.slice('login', 'first_name', 'id').merge(:permissions => @user.permissions.as_json)
149
+ end
150
+
151
+ it "should let me specify methods as well as attributes" do
152
+ User.attr_encodable :login, :first_name, :id, :foobar
153
+ @user.as_json.should == @user.attributes.slice('login', 'first_name', 'id').merge(:foobar => "baz")
154
+ end
155
+
156
+ describe "reassigning" do
157
+ it "should let me reassign attributes" do
158
+ User.attr_encodable :id => :identifier
159
+ @user.as_json.should == {'identifier' => @user.id}
160
+ end
161
+
162
+ it "should let me reassign attributes alongside regular attributes" do
163
+ User.attr_encodable :login, :last_name, :id => :identifier
164
+ @user.as_json.should == {'identifier' => 1, 'login' => 'flipsasser', 'last_name' => 'sasser'}
165
+ end
166
+
167
+ it "should let me reassign multiple attributes with one delcaration" do
168
+ User.attr_encodable :id => :identifier, :first_name => :foobar
169
+ @user.as_json.should == {'identifier' => 1, 'foobar' => 'flip'}
170
+ end
171
+
172
+ it "should let me reassign :methods" do
173
+ User.attr_encodable :foobar => :w00t
174
+ @user.as_json.should == {'w00t' => 'baz'}
175
+ end
176
+
177
+ it "should let me reassign :include" do
178
+ User.attr_encodable :permissions => :deez_permissions
179
+ @user.as_json.should == {'deez_permissions' => @user.permissions.as_json}
180
+ end
181
+
182
+ it "should let me specify a prefix to a set of attr_encodable's" do
183
+ User.attr_encodable :id, :first_name, :foobar, :permissions, :prefix => :t
184
+ @user.as_json.should == {'t_id' => @user.id, 't_first_name' => @user.first_name, 't_foobar' => 'baz', 't_permissions' => @user.permissions.as_json}
185
+ end
186
+ end
187
+
188
+ it "should propagate down subclasses as well" do
189
+ User.attr_encodable :name
190
+ class SubUser < User; end
191
+ SubUser.unencodable_attributes.should == User.unencodable_attributes
192
+ end
193
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attr_encodable
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 6
10
+ version: 0.0.6
11
+ platform: ruby
12
+ authors:
13
+ - Flip Sasser
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-15 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rcov
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 41
30
+ segments:
31
+ - 0
32
+ - 9
33
+ - 9
34
+ version: 0.9.9
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 2
48
+ - 0
49
+ version: "2.0"
50
+ type: :development
51
+ version_requirements: *id002
52
+ description: "\n attr_encodable enables you to set up defaults for what is included or excluded when you serialize an ActiveRecord object. This is especially useful for protecting private attributes when building a public API.\n "
53
+ email: flip@x451.com
54
+ executables: []
55
+
56
+ extensions: []
57
+
58
+ extra_rdoc_files:
59
+ - LICENSE
60
+ - README.md
61
+ files:
62
+ - LICENSE
63
+ - README.md
64
+ - lib/attr_encodable.rb
65
+ - spec/attr_encodable_spec.rb
66
+ has_rdoc: true
67
+ homepage: http://github.com/Plinq/attr_encodable
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options: []
72
+
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ hash: 3
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project:
96
+ rubygems_version: 1.6.2
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: An attribute black- or white-list for ActiveRecord serialization
100
+ test_files:
101
+ - spec/attr_encodable_spec.rb