attr_encodable 0.0.6

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