├── VERSION ├── .gitignore ├── .rspec ├── lib ├── attr_encodable.rb ├── encodable │ ├── active_record.rb │ ├── array.rb │ └── active_record │ │ ├── instance_methods.rb │ │ └── class_methods.rb └── encodable.rb ├── .rvmrc ├── autotest └── discover.rb ├── CHANGES.md ├── LICENSE ├── Guardfile ├── attr_encodable.gemspec ├── Rakefile ├── README.md └── spec └── attr_encodable_spec.rb /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | pkg/* 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --backtrace 2 | --colour 3 | -------------------------------------------------------------------------------- /lib/attr_encodable.rb: -------------------------------------------------------------------------------- 1 | require 'encodable' -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.3@attr_encodable --create 2 | -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | require 'autotest/fsevent' 2 | require 'autotest/growl' 3 | 4 | Autotest.add_discovery { "rspec2" } 5 | -------------------------------------------------------------------------------- /lib/encodable/active_record.rb: -------------------------------------------------------------------------------- 1 | require 'encodable/active_record/class_methods' 2 | require 'encodable/active_record/instance_methods' 3 | 4 | module Encodable 5 | module ActiveRecord 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/encodable/array.rb: -------------------------------------------------------------------------------- 1 | Array.class_eval do 2 | def as_json_with_encodable(name = nil, options = nil) 3 | case name 4 | when Hash, NilClass 5 | options = name 6 | when String, Symbol 7 | (options ||= {}).merge! :as => name 8 | end 9 | as_json_without_encodable options 10 | end 11 | alias_method_chain :as_json, :encodable 12 | end 13 | -------------------------------------------------------------------------------- /lib/encodable.rb: -------------------------------------------------------------------------------- 1 | module Encodable 2 | autoload(:ActiveRecord, 'encodable/active_record') 3 | autoload(:Array, 'encodable/array') 4 | end 5 | 6 | if defined? ActiveRecord::Base 7 | ActiveRecord::Base.extend Encodable::ActiveRecord::ClassMethods 8 | ActiveRecord::Base.send :include, Encodable::ActiveRecord::InstanceMethods 9 | require 'encodable/array' 10 | end 11 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # 0.1.2 2 | - Added scopes to `:as` options. This adds Class.as_name, which automatically restricts the SELECT to encodable attributes ONLY. 3 | 4 | # 0.1.1 5 | - Added support for `:as` in `attr_encodable` calls, allowing you to create custom default encoding groups. See README.md for more. 6 | 7 | # 0.1.0 8 | - Added support for `:only` to to_json, just like you'd expect. Now passing `:only` will exclude all other whitelisted attributes and, as always, 9 | includes inline-support for `:method` and `:include`. 10 | - Removed accidental redis requirement from the Gemspec. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 21 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec', :version => 2 do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | 9 | # Rails example 10 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 11 | watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 12 | watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } 13 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 14 | watch('config/routes.rb') { "spec/routing" } 15 | watch('app/controllers/application_controller.rb') { "spec/controllers" } 16 | 17 | # Capybara request specs 18 | watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } 19 | 20 | # Turnip features and steps 21 | watch(%r{^spec/acceptance/(.+)\.feature$}) 22 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' } 23 | end 24 | 25 | -------------------------------------------------------------------------------- /attr_encodable.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "attr_encodable" 8 | s.version = "0.1.2" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Flip Sasser"] 12 | s.date = "2012-10-16" 13 | s.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\n for protecting private attributes when building a public API.\n " 14 | s.email = "flip@x451.com" 15 | s.extra_rdoc_files = [ 16 | "LICENSE", 17 | "README.md" 18 | ] 19 | s.files = [ 20 | "LICENSE", 21 | "README.md", 22 | "lib/attr_encodable.rb", 23 | "lib/encodable.rb", 24 | "lib/encodable/active_record.rb", 25 | "lib/encodable/active_record/class_methods.rb", 26 | "lib/encodable/active_record/instance_methods.rb", 27 | "lib/encodable/array.rb" 28 | ] 29 | s.homepage = "http://github.com/Plinq/attr_encodable" 30 | s.require_paths = ["lib"] 31 | s.rubygems_version = "1.8.24" 32 | s.summary = "An attribute black- or white-list for ActiveRecord serialization" 33 | s.test_files = ["spec/attr_encodable_spec.rb"] 34 | 35 | if s.respond_to? :specification_version then 36 | s.specification_version = 3 37 | 38 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 39 | s.add_development_dependency(%q, [">= 2.0"]) 40 | else 41 | s.add_dependency(%q, [">= 2.0"]) 42 | end 43 | else 44 | s.add_dependency(%q, [">= 2.0"]) 45 | end 46 | end 47 | 48 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | begin 3 | require 'rspec/core' 4 | require 'rspec/core/rake_task' 5 | rescue MissingSourceFile 6 | module RSpec 7 | module Core 8 | class RakeTask 9 | def initialize(name) 10 | task name do 11 | # if rspec-rails is a configured gem, this will output helpful material and exit ... 12 | require File.expand_path(File.dirname(__FILE__) + "/../../config/environment") 13 | 14 | # ... otherwise, do this: 15 | raise <<-MSG 16 | 17 | #{"*" * 80} 18 | * You are trying to run an rspec rake task defined in 19 | * #{__FILE__}, 20 | * but rspec can not be found. Try running 'gem install rspec'. 21 | #{"*" * 80} 22 | MSG 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | 30 | Rake.application.instance_variable_get('@tasks').delete('default') 31 | 32 | task :default => :spec 33 | 34 | desc "Run all specs in spec directory" 35 | RSpec::Core::RakeTask.new(:spec) 36 | 37 | namespace :spec do 38 | desc "Run all specs with rcov" 39 | RSpec::Core::RakeTask.new(:coverage) do |t| 40 | t.pattern = 'spec/**/*_spec.rb' 41 | t.rcov = true 42 | t.rcov_opts = %w{--exclude osx\/objc,gems\/,spec\/,features\/} 43 | t.verbose = true 44 | end 45 | end 46 | 47 | require "jeweler" 48 | Jeweler::Tasks.new do |gemspec| 49 | gemspec.name = "attr_encodable" 50 | gemspec.summary = "An attribute black- or white-list for ActiveRecord serialization" 51 | gemspec.files = Dir["{lib}/**/*", "LICENSE", "README.md"] 52 | gemspec.description = %{ 53 | attr_encodable enables you to set up defaults for what is included or excluded when you serialize an ActiveRecord object. This is especially useful 54 | for protecting private attributes when building a public API. 55 | } 56 | gemspec.email = "flip@x451.com" 57 | gemspec.homepage = "http://github.com/Plinq/attr_encodable" 58 | gemspec.authors = ["Flip Sasser"] 59 | gemspec.test_files = Dir["{spec}/**/*"] 60 | gemspec.add_development_dependency 'rspec', '>= 2.0' 61 | end 62 | -------------------------------------------------------------------------------- /lib/encodable/active_record/instance_methods.rb: -------------------------------------------------------------------------------- 1 | module Encodable 2 | module ActiveRecord 3 | module InstanceMethods 4 | def as_json(name = nil, options = nil) 5 | case name 6 | when Hash, NilClass 7 | options = name 8 | when String, Symbol 9 | (options ||= {}).merge! :as => name 10 | end 11 | super options 12 | end 13 | 14 | def serializable_hash(options = {}) 15 | options ||= {} 16 | options[:as] ||= :default 17 | 18 | original_except = if options[:except] 19 | options[:except] = Array(options[:except]).map(&:to_sym) 20 | else 21 | options[:except] = [] 22 | end 23 | 24 | # Convert :only to :except 25 | if options && options[:only] 26 | options[:except].push *self.class.default_attributes(options[:as]) - Array(options.delete(:only).map(&:to_sym)) 27 | end 28 | 29 | # This is a little bit confusing. ActiveRecord's default behavior is to apply the :except arguments you pass 30 | # in to any :include options UNLESS it's overridden on the :include option. In the event that we have some 31 | # *default* excepts that come from Encodable, we want to ignore those and pass only whatever the original 32 | # :except options from the user were on down to the :include guys. 33 | inherited_except = original_except - self.class.default_attributes(options[:as]) 34 | case options[:include] 35 | when Array, Symbol 36 | # Convert includes arrays or singleton symbols into a hash with our original_except scope 37 | includes = Array(options[:include]) 38 | options[:include] = Hash[*includes.map{|association| [association, {:except => inherited_except}]}.flatten] 39 | else 40 | options[:include] ||= {} 41 | end 42 | # Exclude the black-list 43 | options[:except].push *self.class.unencodable_attributes(options[:as]) 44 | # Include any default :include or :methods arguments that were passed in earlier 45 | self.class.default_attributes(options[:as]).each do |attribute, as| 46 | unless options[:except].include?(attribute) 47 | if association = self.class.reflect_on_association(attribute) 48 | options[:include][attribute] = {:except => inherited_except} 49 | elsif respond_to?(attribute) && !self.class.column_names.include?(attribute.to_s) 50 | options[:methods] ||= Array(options[:methods]).compact 51 | options[:methods].push attribute 52 | end 53 | end 54 | end 55 | as_json = super(options) 56 | self.class.renamed_encoded_attributes(options[:as]).each do |attribute, as| 57 | if as_json.has_key?(attribute) || as_json.has_key?(attribute.to_s) 58 | as_json[as.to_s] = as_json.delete(attribute) || as_json.delete(attribute.to_s) 59 | end 60 | end 61 | as_json 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/encodable/active_record/class_methods.rb: -------------------------------------------------------------------------------- 1 | module Encodable 2 | module ActiveRecord 3 | module ClassMethods 4 | def attr_encodable(*attributes) 5 | options = extract_encodable_options!(attributes) 6 | 7 | unless @encodable_whitelist_started 8 | # Since we're white-listing, make sure we black-list every attribute to begin with 9 | unencodable_attributes(options[:as]).push *column_names.map(&:to_sym) 10 | @encodable_whitelist_started = true 11 | end 12 | 13 | attributes.each do |attribute| 14 | if attribute.is_a?(Hash) 15 | attribute.each do |method, value| 16 | add_encodable_attribute(method, value, options) 17 | end 18 | else 19 | add_encodable_attribute(attribute, attribute, options) 20 | end 21 | end 22 | 23 | if options[:as] != :default 24 | scope options[:as], select(default_attributes(options[:as]).reject{|attribute| !column_names.include?(attribute.to_s)}) 25 | end 26 | end 27 | 28 | def add_encodable_attribute(method, value, options = {}) 29 | value = "#{options[:prefix]}_#{value}" if options[:prefix] 30 | method = method.to_sym 31 | value = value.to_sym 32 | renamed_encoded_attributes(options[:as]).merge!({method => value}) if method != value 33 | # Un-black-list any attribute we white-listed 34 | unencodable_attributes(options[:as]).delete method 35 | default_attributes(options[:as]).push method 36 | end 37 | 38 | def attr_unencodable(*attributes) 39 | options = extract_encodable_options!(attributes) 40 | unencodable_attributes(options[:as]).push *attributes.map(&:to_sym) 41 | end 42 | 43 | def default_attributes(name = nil) 44 | @default_attributes ||= merge_encodable_superclass_options(:default_attributes, []) 45 | if name 46 | @default_attributes[name] ||= [] 47 | else 48 | @default_attributes 49 | end 50 | end 51 | 52 | def encodable_sets 53 | @encodable_sets 54 | end 55 | 56 | def renamed_encoded_attributes(name = nil) 57 | @renamed_encoded_attributes ||= merge_encodable_superclass_options(:renamed_encoded_attributes, {}) 58 | if name 59 | @renamed_encoded_attributes[name] ||= {} 60 | else 61 | @renamed_encoded_attributes 62 | end 63 | end 64 | 65 | def unencodable_attributes(name = nil) 66 | @unencodable_attributes ||= merge_encodable_superclass_options(:unencodable_attributes, []) 67 | if name 68 | @unencodable_attributes[name] ||= [] 69 | else 70 | @unencodable_attributes 71 | end 72 | end 73 | 74 | private 75 | def extract_encodable_options!(attributes) 76 | begin 77 | attributes.last.assert_valid_keys(:prefix, :as) 78 | options = attributes.extract_options! 79 | rescue ArgumentError 80 | end if attributes.last.is_a?(Hash) 81 | 82 | options ||= {} 83 | options[:as] ||= :default 84 | options 85 | end 86 | 87 | def merge_encodable_superclass_options(method, default) 88 | value = {} 89 | superk = superclass 90 | while superk.respond_to?(method) 91 | supervalue = superk.send(method) 92 | case default 93 | when Array 94 | supervalue.each {|name, default| (value[name] ||= []).push *default } 95 | when Hash 96 | supervalue.each {|name, default| (value[name] ||= {}).merge! default } 97 | end 98 | superk = superk.superclass 99 | end 100 | value 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # attr_encodable 2 | 3 | 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. 4 | 5 | ## Install 6 | 7 | Bundler: 8 | 9 | gem 'attr_encodable' 10 | 11 | Rubygems: 12 | 13 | gem install attr_encodable 14 | 15 | 16 | ## Usage 17 | 18 | ### White-listing 19 | 20 | 21 | 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: 22 | 23 | ```ruby 24 | class User < ActiveRecord::Base 25 | has_many :permissions 26 | validates_presence_of :email, :password 27 | 28 | def foobar 29 | "baz" 30 | end 31 | end 32 | 33 | class Permission < ActiveRecord::Base 34 | belongs_to :user 35 | validates_presence_of :name, :user 36 | 37 | def hello 38 | "World!" 39 | end 40 | end 41 | ``` 42 | 43 | ... with the following schema: 44 | 45 | ```ruby 46 | create_table :permissions, :force => true do |t| 47 | t.belongs_to :user 48 | t.string :name 49 | end 50 | 51 | create_table :users, :force => true do |t| 52 | t.string :login, :limit => 48 53 | t.string :email, :limit => 128 54 | t.string :name, :limit => 32 55 | t.string :password, :limit => 60 56 | t.boolean :admin, :default => false 57 | end 58 | ``` 59 | 60 | Let's make a user and try encoding them: 61 | 62 | ```ruby 63 | @user = User.create(:name => "Flip", :email => "flip@x451.com", :password => "awesomesauce", :admin => true) 64 | #=> # 65 | @user.to_json 66 | #=> {"name":"Flip","admin":true,"id":1,"password":"awesomesauce","login":null,"email":"flip@x451.com"} 67 | ``` 68 | 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? 69 | 70 | ```ruby 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 | 76 | Ah, that's so much better! Now whenever we encode a user instance we'll be showing only some default information. 77 | 78 | `attr_unencodable` is similar, except that it bans an attribute. Following along with the example above, if we then called `attr_unencodable`, we could 79 | restrict our user's information even more. Let's say I don't want my e-mail getting out: 80 | 81 | ```ruby 82 | User.attr_unencodable :email 83 | @user.to_json 84 | #=> {"name":"Flip","id":1,"login":null} 85 | ``` 86 | 87 | Alright! Now you can't see my e-mail. Sucker. 88 | 89 | ### Default `:include` and `:method` options 90 | 91 | `to_json` isn't just concerned with attributes. It also supports `:include`, which includes a relationship with `to_json` called on **it**, as well as `:methods`, which adds the result of calling one or more methods on the instance. `attr_encodable` supports both without specifying what you want to call; just include them in your list: 92 | 93 | ```ruby 94 | User.attr_encodable :foobar 95 | @user.to_json 96 | #=> {"name":"Flip","foobar":"baz","id":1,"login":null} 97 | ``` 98 | 99 | With includes, our example might look like this: 100 | 101 | ```ruby 102 | class User < ActiveRecord::Base 103 | attr_encodable :id, :name, :login, :permissions 104 | has_many :permissions 105 | end 106 | 107 | @user.to_json 108 | #=> {"name":"Flip","foobar":"baz","id":1,"login":null,"permissions":[]} 109 | ``` 110 | 111 | Neato! And of course, when `:permissions` is serialized, it will take into account any `attr_encodable` settings the `Permission` class has! 112 | 113 | ### Renaming Attributes 114 | 115 | 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. 116 | 117 | #### Prefix it! 118 | 119 | **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: 120 | 121 | ```ruby 122 | class User < ActiveRecord::Base 123 | attr_encodable :ed, :prefix => :i_will_hunt 124 | end 125 | 126 | @user.to_json 127 | => {"i_will_hunt_ed":true} 128 | ``` 129 | 130 | #### Rename it completely! 131 | 132 | If you don't want to prefix, just rename the whole damn thing: 133 | 134 | ```ruby 135 | class User < ActiveRecord::Base 136 | attr_encodable :admin => :superuser 137 | end 138 | 139 | @user.to_json 140 | #=> {"superuser":true} 141 | ``` 142 | 143 | Renaming and prefixing work for any `:include` and `:methods` arguments you pass in as well! 144 | 145 | ### NEW! `attr_encodable` groups 146 | 147 | Soemtimes you may want to supply more information or less information, depending on the context. For example, if your API supports listing multiple records and viewing individual records, you may want to list multiple records with just enough information to get them to a URL where they can visit the individual record in detail. In that case, you can create a group using an `:as` option: 148 | 149 | ```ruby 150 | User.attr_encodable :login, :name, :email 151 | User.attr_encodable :login, :as => :listing 152 | ``` 153 | 154 | This will create two groups: the default group, which is how your User will normally be serialized when you call `as_json` or `to_json` on it. Then, the `:listing` group, which can be used like so: 155 | 156 | ```ruby 157 | @user.as_json #=> {"login": "flipsasser", "email": "support@getplinq.com", "name": "Flip Sasser"} 158 | @user.as_json(:listing) #=> {"login": "flipsasser"} 159 | ``` 160 | 161 | This comes in super handy when you want a quick way to limit or expand data in certain situations. 162 | 163 | To flip the example around, imagine you wanted to default to a very limited set of information, but expand it in a certain situation: 164 | 165 | ```ruby 166 | User.attr_encodable :login 167 | User.attr_encodable :login, :admin, :email, :password, :as => :admin_api 168 | ``` 169 | 170 | Now you can call `@user.to_json(:admin_api)` somewhere, which will include a full users' details, but any other `as_json` call will keep that information private. 171 | 172 | #### Scopes 173 | 174 | The use of `:as` also creates a scope on the class which is a SELECT limited only to those columns the class knows about. This enables higher-performance API calls out-of-the-box. 175 | 176 | Using the first example from above, calling `User.listing` would result in a `SELECT login FROM users` instead of the normal `SELECT * FROM users`. Since 177 | you're only going to be encoding the information from attr_encodable anyway, there's no sense in selecting anything else! 178 | 179 | Okay, that's all. Thanks for stopping by. 180 | 181 | Copyright © 2011 Flip Sasser 182 | 183 | -------------------------------------------------------------------------------- /spec/attr_encodable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'active_support' 3 | require File.join(File.dirname(__FILE__), '..', 'lib', 'attr_encodable') 4 | 5 | describe Encodable do 6 | it "should automatically extend ActiveRecord::Base" do 7 | ActiveRecord::Base.should respond_to(:attr_encodable) 8 | ActiveRecord::Base.should respond_to(:attr_unencodable) 9 | end 10 | 11 | before :each do 12 | ActiveRecord::Base.include_root_in_json = false 13 | ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:', :pool => 5, :timeout => 5000}) 14 | class ::Permission < ActiveRecord::Base; belongs_to :user; def hello; "World!"; end; end 15 | class ::User < ActiveRecord::Base; has_many :permissions; def foobar; "baz"; end; end 16 | silence_stream(STDOUT) do 17 | ActiveRecord::Schema.define do 18 | create_table :permissions, :force => true do |t| 19 | t.belongs_to :user 20 | t.string :name 21 | end 22 | create_table :users, :force => true do |t| 23 | t.string "login", :limit => 48 24 | t.string "email", :limit => 128 25 | t.string "first_name", :limit => 32 26 | t.string "last_name", :limit => 32 27 | t.string "encrypted_password", :limit => 60 28 | t.boolean "developer", :default => false 29 | t.boolean "admin", :default => false 30 | t.boolean "password_set", :default => true 31 | t.boolean "verified", :default => false 32 | t.datetime "created_at" 33 | t.datetime "updated_at" 34 | t.integer "notifications" 35 | end 36 | end 37 | end 38 | @user = User.create({ 39 | :login => "flipsasser", 40 | :first_name => "flip", 41 | :last_name => "sasser", 42 | :email => "flip@foobar.com", 43 | :encrypted_password => SecureRandom.hex(30), 44 | :developer => true, 45 | :admin => true, 46 | :password_set => true, 47 | :verified => true, 48 | :notifications => 7 49 | }) 50 | @user.permissions.create(:name => "create_blog_posts") 51 | @user.permissions.create(:name => "edit_blog_posts") 52 | # Reset the options for each test 53 | [Permission, User].each do |klass| 54 | 55 | klass.class_eval do 56 | @default_attributes = nil 57 | @encodable_whitelist_started = nil 58 | @renamed_encoded_attributes = nil 59 | @unencodable_attributes = nil 60 | end 61 | end 62 | end 63 | 64 | it "should favor whitelisting to blacklisting" do 65 | User.unencodable_attributes(:default).should == [] 66 | User.attr_unencodable 'foo', 'bar', 'baz' 67 | User.unencodable_attributes(:default).should == [:foo, :bar, :baz] 68 | User.attr_encodable :id, :first_name 69 | User.unencodable_attributes(:default).map(&:to_s).should == ['foo', 'bar', 'baz'] + User.column_names - ['id', 'first_name'] 70 | end 71 | 72 | describe "at the parent model level" do 73 | it "should not mess with to_json unless when attr_encodable and attr_unencodable are not set" do 74 | @user.as_json.should == @user.attributes 75 | end 76 | 77 | it "should not mess with :include options" do 78 | @user.as_json(:include => :permissions).should == @user.attributes.merge(:permissions => @user.permissions.as_json) 79 | end 80 | 81 | it "should not mess with :methods options" do 82 | @user.as_json(:methods => :foobar).should == @user.attributes.merge(:foobar => "baz") 83 | end 84 | 85 | it "should allow me to whitelist attributes" do 86 | User.attr_encodable :login, :first_name, :last_name 87 | @user.as_json.should == @user.attributes.slice('login', 'first_name', 'last_name') 88 | end 89 | 90 | it "should allow me to blacklist attributes" do 91 | User.attr_unencodable :login, :first_name, :last_name 92 | @user.as_json.should == @user.attributes.except('login', 'first_name', 'last_name') 93 | end 94 | 95 | 96 | # Of note is the INSANITY of ActiveRecord in that it applies :only / :except to :include as well. Which is 97 | # obviously insane. Similarly, it doesn't allow :methods to come along when :only is specified. Good god, what 98 | # a shame. 99 | it "should allow me to whitelist attributes without messing with :include" do 100 | User.attr_encodable :login, :first_name, :last_name 101 | @user.as_json(:include => :permissions).should == @user.attributes.slice('login', 'first_name', 'last_name').merge(:permissions => @user.permissions.as_json) 102 | end 103 | 104 | it "should allow me to blacklist attributes without messing with :include and :methods" do 105 | User.attr_unencodable :login, :first_name, :last_name 106 | @user.as_json(:include => :permissions, :methods => :foobar).should == @user.attributes.except('login', 'first_name', 'last_name').merge(:permissions => @user.permissions.as_json, :foobar => "baz") 107 | end 108 | 109 | it "should not screw with :include if it's a hash" do 110 | User.attr_unencodable :login, :first_name, :last_name 111 | @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") 112 | end 113 | end 114 | 115 | describe "at the child model level when the parent model has attr_encodable set" do 116 | before :each do 117 | User.attr_encodable :login, :first_name, :last_name 118 | end 119 | 120 | 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 121 | @user.permissions.as_json.should == @user.permissions.map(&:attributes) 122 | end 123 | 124 | it "should not mess with :include options" do 125 | # This is testing that the implicit ban on the :id attribute from User.attr_encodable is not 126 | # applying to serialization of permissions 127 | @user.as_json(:include => :permissions)[:permissions].first['id'].should_not be_nil 128 | end 129 | 130 | it "should inherit any attr_encodable options from the child model" do 131 | User.attr_encodable :id 132 | Permission.attr_encodable :name 133 | as_json = @user.as_json(:include => :permissions) 134 | as_json[:permissions].first['id'].should be_nil 135 | as_json['id'].should_not be_nil 136 | end 137 | 138 | # it "should allow me to whitelist attributes" do 139 | # User.attr_encodable :login, :first_name, :last_name 140 | # @user.as_json.should == @user.attributes.slice('login', 'first_name', 'last_name') 141 | # end 142 | # 143 | # it "should allow me to blacklist attributes" do 144 | # User.attr_unencodable :login, :first_name, :last_name 145 | # @user.as_json.should == @user.attributes.except('login', 'first_name', 'last_name') 146 | # end 147 | end 148 | 149 | it "should let me specify automatic includes as well as attributes" do 150 | User.attr_encodable :login, :first_name, :id, :permissions 151 | @user.as_json.should == @user.attributes.slice('login', 'first_name', 'id').merge(:permissions => @user.permissions.as_json) 152 | end 153 | 154 | it "should let me specify methods as well as attributes" do 155 | User.attr_encodable :login, :first_name, :id, :foobar 156 | @user.as_json.should == @user.attributes.slice('login', 'first_name', 'id').merge(:foobar => "baz") 157 | end 158 | 159 | it "should allow me to only request certain whitelisted attributes and methods" do 160 | User.attr_encodable :login, :first_name, :last_name, :foobar 161 | @user.as_json(:only => [:login, :foobar]).should == {'login' => 'flipsasser', :foobar => 'baz'} 162 | end 163 | 164 | it "should allow me to use :only with aliased methods and attributes" do 165 | User.attr_encodable :login => :login_eh, :first_name => :foist, :last_name => :last, :foobar => :baz 166 | @user.as_json(:only => [:login, :foobar]).should == {'login_eh' => 'flipsasser', 'baz' => 'baz'} 167 | end 168 | 169 | describe "reassigning" do 170 | it "should let me reassign attributes" do 171 | User.attr_encodable :id => :identifier 172 | @user.as_json.should == {'identifier' => @user.id} 173 | end 174 | 175 | it "should let me reassign attributes alongside regular attributes" do 176 | User.attr_encodable :login, :last_name, :id => :identifier 177 | @user.as_json.should == {'identifier' => 1, 'login' => 'flipsasser', 'last_name' => 'sasser'} 178 | end 179 | 180 | it "should let me reassign multiple attributes with one delcaration" do 181 | User.attr_encodable :id => :identifier, :first_name => :foobar 182 | @user.as_json.should == {'identifier' => 1, 'foobar' => 'flip'} 183 | end 184 | 185 | it "should let me reassign :methods" do 186 | User.attr_encodable :foobar => :w00t 187 | @user.as_json.should == {'w00t' => 'baz'} 188 | end 189 | 190 | it "should let me reassign :include" do 191 | User.attr_encodable :permissions => :deez_permissions 192 | @user.as_json.should == {'deez_permissions' => @user.permissions.as_json} 193 | end 194 | 195 | it "should let me specify a prefix to a set of attr_encodable's" do 196 | User.attr_encodable :id, :first_name, :foobar, :permissions, :prefix => :t 197 | @user.as_json.should == {'t_id' => @user.id, 't_first_name' => @user.first_name, 't_foobar' => 'baz', 't_permissions' => @user.permissions.as_json} 198 | end 199 | end 200 | 201 | it "should propagate down subclasses as well" do 202 | User.attr_encodable :name 203 | class SubUser < User; end 204 | SubUser.unencodable_attributes.should == User.unencodable_attributes 205 | end 206 | 207 | describe "named groups" do 208 | it "should be supported on a class-basis with a :name option" do 209 | User.attr_unencodable :id 210 | User.all.as_json.should == [@user.attributes.except('id')] 211 | User.attr_encodable :id, :first_name, :last_name, :as => :short 212 | User.all.as_json(:short).should == [{'id' => 1, 'first_name' => 'flip', 'last_name' => 'sasser'}] 213 | end 214 | 215 | it "should be supported on an instance-basis with a :name option" do 216 | User.attr_encodable :id, :first_name, :last_name, :as => :short 217 | @user.as_json.should == @user.attributes 218 | @user.as_json(:short).should == {'id' => 1, 'first_name' => 'flip', 'last_name' => 'sasser'} 219 | end 220 | 221 | it "should also create a named_scope that limits the SELECT statement to the included attributes" do 222 | User.attr_encodable :id, :as => :short 223 | User.first.first_name.should == 'flip' 224 | lambda { User.short.first.first_name }.should raise_error(ActiveModel::MissingAttributeError) 225 | User.short.first.id.should == 1 226 | end 227 | end 228 | end 229 | --------------------------------------------------------------------------------