assignable_values 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +3 -0
  4. data/README.md +99 -0
  5. data/Rakefile +11 -0
  6. data/assignable_values.gemspec +22 -0
  7. data/lib/assignable_values.rb +6 -0
  8. data/lib/assignable_values/active_record.rb +25 -0
  9. data/lib/assignable_values/active_record/restriction/base.rb +152 -0
  10. data/lib/assignable_values/active_record/restriction/belongs_to_association.rb +41 -0
  11. data/lib/assignable_values/active_record/restriction/scalar_attribute.rb +28 -0
  12. data/lib/assignable_values/errors.rb +5 -0
  13. data/lib/assignable_values/version.rb +3 -0
  14. data/spec/app_root/.gitignore +4 -0
  15. data/spec/app_root/app/controllers/application_controller.rb +2 -0
  16. data/spec/app_root/app/models/artist.rb +5 -0
  17. data/spec/app_root/app/models/song.rb +5 -0
  18. data/spec/app_root/config/application.rb +31 -0
  19. data/spec/app_root/config/boot.rb +13 -0
  20. data/spec/app_root/config/database.yml +4 -0
  21. data/spec/app_root/config/environment.rb +5 -0
  22. data/spec/app_root/config/environments/in_memory.rb +0 -0
  23. data/spec/app_root/config/environments/mysql.rb +0 -0
  24. data/spec/app_root/config/environments/postgresql.rb +0 -0
  25. data/spec/app_root/config/environments/sqlite.rb +0 -0
  26. data/spec/app_root/config/environments/sqlite3.rb +0 -0
  27. data/spec/app_root/config/environments/test.rb +35 -0
  28. data/spec/app_root/config/initializers/backtrace_silencers.rb +7 -0
  29. data/spec/app_root/config/initializers/inflections.rb +10 -0
  30. data/spec/app_root/config/initializers/mime_types.rb +5 -0
  31. data/spec/app_root/config/initializers/secret_token.rb +7 -0
  32. data/spec/app_root/config/initializers/session_store.rb +8 -0
  33. data/spec/app_root/config/locales/en.yml +10 -0
  34. data/spec/app_root/config/routes.rb +58 -0
  35. data/spec/app_root/db/migrate/001_create_artists.rb +11 -0
  36. data/spec/app_root/db/migrate/002_create_songs.rb +15 -0
  37. data/spec/app_root/lib/tasks/.gitkeep +0 -0
  38. data/spec/app_root/script/rails +6 -0
  39. data/spec/assignable_values/active_record_spec.rb +385 -0
  40. data/spec/rcov.opts +2 -0
  41. data/spec/spec_helper.rb +21 -0
  42. metadata +165 -0
@@ -0,0 +1,8 @@
1
+ doc
2
+ pkg
3
+ *.gem
4
+ .idea
5
+ spec/app_root/log/*
6
+ Gemfile.lock
7
+
8
+
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,99 @@
1
+ # assignable_values - Enums on vitamins
2
+
3
+ `assignable_values` lets you restrict the values that can be assigned to attributes or associations of ActiveRecord models. You can think of it as enums where the list of allowed values is generated at runtime and the value is checked during validation.
4
+
5
+ We carefully enhanced the cure enum functionality with small tweaks that are useful for web forms, internationalized applications and common authorization patterns.
6
+
7
+ ## Restricting attributes
8
+
9
+ The basic usage to restrict the values assignable to strings, integers, etc. is this:
10
+
11
+ class Song < ActiveRecord::Base
12
+ assignable_values_for :genre do
13
+ ['pop', 'rock', 'electronic']
14
+ end
15
+ end
16
+
17
+ The assigned value is checked during validation:
18
+
19
+ Song.new(:genre => 'rock').valid? # => true
20
+ Song.new(:genre => 'elephant').valid? # => false
21
+
22
+ The validation error message is the same as the one from `validates_inclusion_of` (`errors.messages.inclusion` in your I18n dictionary).
23
+
24
+ ### How assignable values are generated
25
+
26
+ The list of assignable values is generated at runtime. Since the given block is evaluated on the record instance, so you can refer to other methods:
27
+
28
+ class Song < ActiveRecord::Base
29
+
30
+ validates_numericality_of :year
31
+
32
+ assignable_values_for :genre do
33
+ genres = []
34
+ genres << 'jazz' if year > 1900
35
+ genres << 'rock' if year > 1960
36
+ genres
37
+ end
38
+
39
+ end
40
+
41
+ ### Obtaining lists
42
+
43
+ You can ask a record for a list of values that can be assigned to attribute:
44
+
45
+ song.assignable_genres # => ['pop', 'rock', 'electronic']
46
+
47
+ This is useful for populating &lt;select&gt; tags in web forms:
48
+
49
+ form.select :genre, form.object.assignable_genres
50
+
51
+ ### Human labels
52
+
53
+ You will often want to present internal values in a humanized form. E.g. `"pop"` should be presented as `"Pop music"`.
54
+
55
+ You can define human labels in your I18n dictionary:
56
+
57
+ en:
58
+ assignable_values:
59
+ song:
60
+ genre:
61
+ pop: 'Pop music'
62
+ rock: 'Rock music'
63
+ electronic: 'Electronic music'
64
+
65
+ When obtaining a list of assignable values, each value will have a method `#human` that returns the translation:
66
+
67
+ song.assignable_genres.first # => 'pop'
68
+ song.assignable_genres.first.human # => 'Pop music'
69
+
70
+ You can populate a &lt;select&gt; tag with pairs of internal values and human labels like this:
71
+
72
+ form.collection_select :genre, form.object.assignable_genres, :to_s, :human
73
+
74
+ ## Restricting belongs_to associations
75
+
76
+ ### No caching
77
+
78
+ ## Defining default values
79
+
80
+ ## Obtaining assignable values from a delegate
81
+
82
+ Text here.
83
+
84
+ ## Previously saved values
85
+
86
+ - Always valid
87
+ - Are listed
88
+
89
+ ## Installation
90
+
91
+ Text here.
92
+
93
+ ## Development
94
+
95
+ Text here.
96
+
97
+ ## Credits
98
+
99
+ Text here.
@@ -0,0 +1,11 @@
1
+ require 'rake'
2
+ require 'rspec/core/rake_task'
3
+ require 'bundler/gem_tasks'
4
+
5
+ desc 'Default: Run all specs.'
6
+ task :default => :spec
7
+
8
+ desc "Run all specs"
9
+ RSpec::Core::RakeTask.new do |t|
10
+ # t.spec_files = FileList['spec/**/*_spec.rb']
11
+ end
@@ -0,0 +1,22 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "assignable_values/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'assignable_values'
6
+ s.version = AssignableValues::VERSION
7
+ s.authors = ["Henning Koch"]
8
+ s.email = 'henning.koch@makandra.de'
9
+ s.homepage = 'https://github.com/makandra/assignable_values'
10
+ s.summary = 'Restrict the values assignable to ActiveRecord attributes or associations. Or enums on steroids.'
11
+ s.description = s.summary
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+
18
+ s.add_development_dependency('rails', '~>3.1')
19
+ s.add_development_dependency('rspec', '~>2.8')
20
+ s.add_development_dependency('rspec-rails', '~>2.8')
21
+ s.add_development_dependency('sqlite3')
22
+ end
@@ -0,0 +1,6 @@
1
+ require 'assignable_values/errors'
2
+ require 'assignable_values/active_record'
3
+ require 'assignable_values/active_record/restriction/base'
4
+ require 'assignable_values/active_record/restriction/belongs_to_association'
5
+ require 'assignable_values/active_record/restriction/scalar_attribute'
6
+
@@ -0,0 +1,25 @@
1
+ module AssignableValues
2
+ module ActiveRecord
3
+
4
+ private
5
+
6
+ def assignable_values_for(property, options = {}, &values)
7
+ restriction_type = belongs_to_association?(property) ? Restriction::BelongsToAssociation : Restriction::ScalarAttribute
8
+ restriction_type.new(self, property, options, &values)
9
+ end
10
+
11
+ #def authorize_values_for(property, options = {})
12
+ # method_defined?(:power) or attr_accessor :power
13
+ # assignable_values_for property, options.merge(:through => :power)
14
+ #end
15
+
16
+ def belongs_to_association?(property)
17
+ reflection = reflect_on_association(property)
18
+ reflection && reflection.macro == :belongs_to
19
+ end
20
+
21
+ end
22
+ end
23
+
24
+ ActiveRecord::Base.send(:extend, AssignableValues::ActiveRecord)
25
+
@@ -0,0 +1,152 @@
1
+ module AssignableValues
2
+ module ActiveRecord
3
+ module Restriction
4
+ class Base
5
+
6
+ attr_reader :model, :property, :options, :values, :default
7
+
8
+ def initialize(model, property, options, &values)
9
+ @model = model
10
+ @property = property
11
+ @options = options
12
+ @values = values
13
+ ensure_values_given
14
+ setup_default if default?
15
+ define_assignable_values_method
16
+ setup_validation
17
+ end
18
+
19
+ def validate_record(record)
20
+ value = current_value(record)
21
+ unless allow_blank? && value.blank?
22
+ begin
23
+ assignable_values = assignable_values(record)
24
+ assignable_values.include?(value) or record.errors.add(property, I18n.t('errors.messages.inclusion'))
25
+ rescue DelegateUnavailable
26
+ # if the delegate is unavailable, the validation is skipped
27
+ end
28
+ end
29
+ end
30
+
31
+ def assignable_values(record)
32
+ assignable_values = []
33
+ old_value = previously_saved_value(record)
34
+ assignable_values << old_value if old_value.present?
35
+ assignable_values |= raw_assignable_values(record)
36
+ assignable_values = decorate_values(assignable_values)
37
+ assignable_values
38
+ end
39
+
40
+ def set_default(record)
41
+ if record.new_record? && record.send(property).nil?
42
+ default_value = default
43
+ default_value = record.instance_eval(&default_value) if default_value.is_a?(Proc)
44
+ record.send("#{property}=", default_value)
45
+ end
46
+ true
47
+ end
48
+
49
+ def humanize_string_value(value)
50
+ I18n.t("assignable_values.#{model.name.underscore}.#{property}.#{value}", :default => value.humanize)
51
+ end
52
+
53
+ private
54
+
55
+ def current_value(record)
56
+ record.send(property)
57
+ end
58
+
59
+ def previously_saved_value(record)
60
+ nil
61
+ end
62
+
63
+ def decorate_values(values)
64
+ values
65
+ end
66
+
67
+ def delegate?
68
+ @options.has_key?(:through)
69
+ end
70
+
71
+ def default?
72
+ @options.has_key?(:default)
73
+ end
74
+
75
+ def allow_blank?
76
+ @options[:allow_blank]
77
+ end
78
+
79
+ def delegate_definition
80
+ options[:through]
81
+ end
82
+
83
+ def enhance_model(&block)
84
+ @model.class_eval(&block)
85
+ end
86
+
87
+ def setup_default
88
+ @default = options[:default]
89
+ restriction = self
90
+ enhance_model do
91
+ set_default_method = "set_default_#{restriction.property}"
92
+ define_method set_default_method do
93
+ restriction.set_default(self)
94
+ end
95
+ after_initialize set_default_method
96
+ end
97
+ end
98
+
99
+ def setup_validation
100
+ restriction = self
101
+ enhance_model do
102
+ validate_method = "validate_#{restriction.property}_assignable"
103
+ define_method validate_method do
104
+ restriction.validate_record(self)
105
+ end
106
+ validate validate_method
107
+ end
108
+ end
109
+
110
+ def define_assignable_values_method
111
+ restriction = self
112
+ enhance_model do
113
+ assignable_values_method = "assignable_#{restriction.property.to_s.pluralize}"
114
+ define_method assignable_values_method do
115
+ restriction.assignable_values(self)
116
+ end
117
+ end
118
+ end
119
+
120
+ def raw_assignable_values(record)
121
+ if delegate?
122
+ assignable_values_from_delegate(record)
123
+ else
124
+ record.instance_eval(&@values)
125
+ end.to_a
126
+ end
127
+
128
+ def delegate(record)
129
+ case delegate_definition
130
+ when Symbol then record.send(delegate_definition)
131
+ when Proc then record.instance_eval(&delegate_definition)
132
+ else raise "Illegal delegate definition: #{delegate_definition.inspect}"
133
+ end
134
+ end
135
+
136
+ def assignable_values_from_delegate(record)
137
+ delegate = delegate(record)
138
+ delegate.present? or raise DelegateUnavailable, "Cannot query a nil delegate for assignable values"
139
+ delegate_query_method = "assignable_#{model.name.underscore}_#{property.to_s.pluralize}"
140
+ args = delegate.method(delegate_query_method).arity == 1 ? [record] : []
141
+ delegate.send(delegate_query_method, *args)
142
+ end
143
+
144
+ def ensure_values_given
145
+ @values or @options[:through] or raise NoValuesGiven, 'You must supply the list of assignable values by either a block or :through option'
146
+ end
147
+
148
+ end
149
+ end
150
+ end
151
+ end
152
+
@@ -0,0 +1,41 @@
1
+ module AssignableValues
2
+ module ActiveRecord
3
+ module Restriction
4
+ class BelongsToAssociation < Base
5
+
6
+ private
7
+
8
+ def association_class
9
+ model.reflect_on_association(property).klass
10
+ end
11
+
12
+ def association_id_method
13
+ "#{property}_id"
14
+ end
15
+
16
+ def association_id(record)
17
+ record.send(association_id_method)
18
+ end
19
+
20
+ def previously_saved_value(record)
21
+ if old_id = record.send("#{association_id_method}_was")
22
+ if old_id == association_id(record)
23
+ current_value(record) # no need to query the database if nothing changed
24
+ else
25
+ association_class.find_by_id(old_id)
26
+ end
27
+ end
28
+ end
29
+
30
+ def current_value(record)
31
+ value = record.send(property)
32
+ value = record.send(property, true) if (value && value.id) != association_id(record)
33
+ value
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+
@@ -0,0 +1,28 @@
1
+ module AssignableValues
2
+ module ActiveRecord
3
+ module Restriction
4
+ class ScalarAttribute < Base
5
+
6
+ private
7
+
8
+ def decorate_values(values)
9
+ restriction = self
10
+ values.collect do |value|
11
+ if value.is_a?(String)
12
+ value = value.dup
13
+ value.singleton_class.send(:define_method, :human) do
14
+ restriction.humanize_string_value(value)
15
+ end
16
+ end
17
+ value
18
+ end
19
+ end
20
+
21
+ def previously_saved_value(record)
22
+ record.send("#{property}_was")
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ module AssignableValues
2
+ class Error < StandardError; end
3
+ class DelegateUnavailable < Error; end
4
+ class NoValuesGiven < Error; end
5
+ end
@@ -0,0 +1,3 @@
1
+ module AssignableValues
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ .bundle
2
+ db/*.sqlite3
3
+ log/*.log
4
+ tmp/**/*
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -0,0 +1,5 @@
1
+ class Artist < ActiveRecord::Base
2
+
3
+ has_many :songs
4
+
5
+ end
@@ -0,0 +1,5 @@
1
+ class Song < ActiveRecord::Base
2
+
3
+ belongs_to :artist
4
+
5
+ end
@@ -0,0 +1,31 @@
1
+ require File.expand_path('../boot', __FILE__)
2
+
3
+ require 'rails/all'
4
+
5
+ # If you have a Gemfile, require the gems listed there, including any gems
6
+ # you've limited to :test, :development, or :production.
7
+ Bundler.require(:default, Rails.env) if defined?(Bundler)
8
+
9
+ module AssignableValuesSpecApp
10
+ class Application < Rails::Application
11
+ config.encoding = "utf-8"
12
+
13
+ config.cache_classes = true
14
+ config.whiny_nils = true
15
+
16
+ config.consider_all_requests_local = true
17
+ config.action_controller.perform_caching = false
18
+
19
+ config.action_dispatch.show_exceptions = false
20
+
21
+ config.action_controller.allow_forgery_protection = false
22
+
23
+ config.action_mailer.delivery_method = :test
24
+
25
+ config.active_support.deprecation = :stderr
26
+
27
+ config.root = File.expand_path('../..', __FILE__)
28
+
29
+ # railties.plugins << Rails::Plugin.new(File.expand_path('../../../../..', __FILE__))
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+
3
+ # Set up gems listed in the Gemfile.
4
+ gemfile = File.expand_path('../../Gemfile', __FILE__)
5
+ begin
6
+ ENV['BUNDLE_GEMFILE'] = gemfile
7
+ require 'bundler'
8
+ Bundler.setup
9
+ rescue Bundler::GemNotFound => e
10
+ STDERR.puts e.message
11
+ STDERR.puts "Try running `bundle install`."
12
+ exit!
13
+ end if File.exist?(gemfile)
@@ -0,0 +1,4 @@
1
+ in_memory:
2
+ adapter: sqlite3
3
+ database: ":memory:"
4
+ verbosity: quiet
@@ -0,0 +1,5 @@
1
+ # Load the rails application
2
+ require File.expand_path('../application', __FILE__)
3
+
4
+ # Initialize the rails application
5
+ AssignableValuesSpecApp::Application.initialize!
@@ -0,0 +1,35 @@
1
+ AssignableValuesSpecApp::Application.configure do
2
+ # Settings specified here will take precedence over those in config/application.rb
3
+
4
+ # The test environment is used exclusively to run your application's
5
+ # test suite. You never need to work with it otherwise. Remember that
6
+ # your test database is "scratch space" for the test suite and is wiped
7
+ # and recreated between test runs. Don't rely on the data there!
8
+ config.cache_classes = false
9
+
10
+ # Log error messages when you accidentally call methods on nil.
11
+ config.whiny_nils = true
12
+
13
+ # Show full error reports and disable caching
14
+ config.consider_all_requests_local = true
15
+ config.action_controller.perform_caching = false
16
+
17
+ # Raise exceptions instead of rendering exception templates
18
+ config.action_dispatch.show_exceptions = false
19
+
20
+ # Disable request forgery protection in test environment
21
+ config.action_controller.allow_forgery_protection = false
22
+
23
+ # Tell Action Mailer not to deliver emails to the real world.
24
+ # The :test delivery method accumulates sent emails in the
25
+ # ActionMailer::Base.deliveries array.
26
+ config.action_mailer.delivery_method = :test
27
+
28
+ # Use SQL instead of Active Record's schema dumper when creating the test database.
29
+ # This is necessary if your schema can't be completely dumped by the schema dumper,
30
+ # like if you have constraints or database-specific column types
31
+ # config.active_record.schema_format = :sql
32
+
33
+ # Print deprecation notices to the stderr
34
+ config.active_support.deprecation = :stderr
35
+ end
@@ -0,0 +1,7 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4
+ # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5
+
6
+ # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7
+ # Rails.backtrace_cleaner.remove_silencers!
@@ -0,0 +1,10 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Add new inflection rules using the following format
4
+ # (all these examples are active by default):
5
+ # ActiveSupport::Inflector.inflections do |inflect|
6
+ # inflect.plural /^(ox)$/i, '\1en'
7
+ # inflect.singular /^(ox)en/i, '\1'
8
+ # inflect.irregular 'person', 'people'
9
+ # inflect.uncountable %w( fish sheep )
10
+ # end
@@ -0,0 +1,5 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Add new mime types for use in respond_to blocks:
4
+ # Mime::Type.register "text/richtext", :rtf
5
+ # Mime::Type.register_alias "text/html", :iphone
@@ -0,0 +1,7 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Your secret key for verifying the integrity of signed cookies.
4
+ # If you change this key, all old signed cookies will become invalid!
5
+ # Make sure the secret is at least 30 characters and all random,
6
+ # no regular words or you'll be exposed to dictionary attacks.
7
+ AssignableValuesSpecApp::Application.config.secret_token = 'cb014a08a45243e7143f31e04774c342c1fba329fd594ae1a480d8283b1a851f425dc08044311fb4be6d000b6e6681de7c76d19148419a5ffa0a9f84556d3b33'
@@ -0,0 +1,8 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ AssignableValuesSpecApp::Application.config.session_store :cookie_store, :key => '_app_root_session'
4
+
5
+ # Use the database for sessions instead of the cookie-based default,
6
+ # which shouldn't be used to store highly confidential information
7
+ # (create the session table with "rails generate session_migration")
8
+ # AssignableValuesSpecApp::Application.config.session_store :active_record_store
@@ -0,0 +1,10 @@
1
+ # Sample localization file for English. Add more files in this directory for other locales.
2
+ # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3
+
4
+ en:
5
+ assignable_values:
6
+ song:
7
+ genre:
8
+ pop: 'Pop music'
9
+ rock: 'Rock music'
10
+
@@ -0,0 +1,58 @@
1
+ AssignableValuesSpecApp::Application.routes.draw do
2
+ # The priority is based upon order of creation:
3
+ # first created -> highest priority.
4
+
5
+ # Sample of regular route:
6
+ # match 'products/:id' => 'catalog#view'
7
+ # Keep in mind you can assign values other than :controller and :action
8
+
9
+ # Sample of named route:
10
+ # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
11
+ # This route can be invoked with purchase_url(:id => product.id)
12
+
13
+ # Sample resource route (maps HTTP verbs to controller actions automatically):
14
+ # resources :products
15
+
16
+ # Sample resource route with options:
17
+ # resources :products do
18
+ # member do
19
+ # get 'short'
20
+ # post 'toggle'
21
+ # end
22
+ #
23
+ # collection do
24
+ # get 'sold'
25
+ # end
26
+ # end
27
+
28
+ # Sample resource route with sub-resources:
29
+ # resources :products do
30
+ # resources :comments, :sales
31
+ # resource :seller
32
+ # end
33
+
34
+ # Sample resource route with more complex sub-resources
35
+ # resources :products do
36
+ # resources :comments
37
+ # resources :sales do
38
+ # get 'recent', :on => :collection
39
+ # end
40
+ # end
41
+
42
+ # Sample resource route within a namespace:
43
+ # namespace :admin do
44
+ # # Directs /admin/products/* to Admin::ProductsController
45
+ # # (app/controllers/admin/products_controller.rb)
46
+ # resources :products
47
+ # end
48
+
49
+ # You can have the root of your site routed with "root"
50
+ # just remember to delete public/index.html.
51
+ # root :to => "welcome#index"
52
+
53
+ # See how all your routes lay out with "rake routes"
54
+
55
+ # This is a legacy wild controller route that's not recommended for RESTful applications.
56
+ # Note: This route will make all actions in every controller accessible via GET requests.
57
+ match ':controller(/:action(/:id(.:format)))'
58
+ end
@@ -0,0 +1,11 @@
1
+ class CreateArtists < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :artists
5
+ end
6
+
7
+ def self.down
8
+ drop_table :artists
9
+ end
10
+
11
+ end
@@ -0,0 +1,15 @@
1
+ class CreateSongs < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :songs do |t|
5
+ t.integer :artist_id
6
+ t.string :genre
7
+ t.integer :year
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ drop_table :songs
13
+ end
14
+
15
+ end
File without changes
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby1.8
2
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3
+
4
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
5
+ require File.expand_path('../../config/boot', __FILE__)
6
+ require 'rails/commands'
@@ -0,0 +1,385 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ describe AssignableValues::ActiveRecord do
5
+
6
+ def disposable_song_class(&block)
7
+ @klass = Class.new(Song, &block)
8
+ @klass.class_eval do
9
+ def self.name
10
+ 'Song'
11
+ end
12
+ end
13
+ @klass
14
+ end
15
+
16
+ describe '.assignable_values' do
17
+
18
+ it 'should raise an error when not called with a block or :through option' do
19
+ expect do
20
+ disposable_song_class do
21
+ assignable_values_for :genre
22
+ end
23
+ end.to raise_error(AssignableValues::NoValuesGiven)
24
+ end
25
+
26
+ context 'when validating scalar attributes' do
27
+
28
+ context 'without options' do
29
+
30
+ before :each do
31
+ @klass = disposable_song_class do
32
+ assignable_values_for :genre do
33
+ %w[pop rock]
34
+ end
35
+ end
36
+ end
37
+
38
+ it 'should validate that the attribute is allowed' do
39
+ @klass.new(:genre => 'pop').should be_valid
40
+ @klass.new(:genre => 'disallowed value').should_not be_valid
41
+ end
42
+
43
+ it 'should use the same error message as validates_inclusion_of' do
44
+ record = @klass.new(:genre => 'disallowed value')
45
+ record.valid?
46
+ record.errors[:genre].first.should == I18n.t('errors.messages.inclusion')
47
+ record.errors[:genre].first.should == 'is not included in the list'
48
+ end
49
+
50
+ it 'should not allow nil for the attribute value' do
51
+ @klass.new(:genre => nil).should_not be_valid
52
+ end
53
+
54
+ it 'should allow a previously saved value even if that value is no longer allowed' do
55
+ song = @klass.new(:genre => 'disallowed value')
56
+ song.save!(:validate => false)
57
+ song.should be_valid
58
+ end
59
+
60
+ end
61
+
62
+ context 'if the :allow_blank option is set' do
63
+
64
+ before :each do
65
+ @klass = disposable_song_class do
66
+ assignable_values_for :genre, :allow_blank => true do
67
+ %w[pop rock]
68
+ end
69
+ end
70
+ end
71
+
72
+ it 'should allow nil for the attribute value' do
73
+ @klass.new(:genre => nil).should be_valid
74
+ end
75
+
76
+ it 'should allow an empty string as value if the :allow_blank option is set' do
77
+ @klass.new(:genre => '').should be_valid
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+
84
+ context 'when validating belongs_to associations' do
85
+
86
+ it 'should validate that the association is allowed' do
87
+ allowed_association = Artist.create!
88
+ disallowed_association = Artist.create!
89
+ @klass = disposable_song_class do
90
+ assignable_values_for :artist do
91
+ [allowed_association]
92
+ end
93
+ end
94
+ @klass.new(:artist => allowed_association).should be_valid
95
+ @klass.new(:artist => disallowed_association).should_not be_valid
96
+ end
97
+
98
+ it 'should allow a nil association if the :allow_blank option is set' do
99
+ @klass = disposable_song_class do
100
+ assignable_values_for :artist, :allow_blank => true do
101
+ []
102
+ end
103
+ end
104
+ record = @klass.new
105
+ record.artist.should be_nil
106
+ record.should be_valid
107
+ end
108
+
109
+ it 'should allow a previously saved association even if that association is no longer allowed' do
110
+ allowed_association = Artist.create!
111
+ disallowed_association = Artist.create!
112
+ @klass = disposable_song_class
113
+ record = @klass.create!(:artist => disallowed_association)
114
+ @klass.class_eval do
115
+ assignable_values_for :artist do
116
+ [allowed_association]
117
+ end
118
+ end
119
+ record.should be_valid
120
+ end
121
+
122
+ it "should not load a previously saved association if the association's foreign key hasn't changed" do
123
+ association = Artist.create!
124
+ @klass = disposable_song_class do
125
+ assignable_values_for :artist do
126
+ [association] # This example doesn't care about what's assignable. We're only interested in behavior up to the validation.
127
+ end
128
+ end
129
+ record = @klass.create!(:artist => association)
130
+ Artist.should_not_receive(:find_by_id)
131
+ record.valid?
132
+ end
133
+
134
+ it 'should not fail or allow nil if a previously saved association no longer exists in the database' do
135
+ allowed_association = Artist.create!
136
+ disposable_song_class.class_eval do
137
+ assignable_values_for :artist do
138
+ [allowed_association]
139
+ end
140
+ end
141
+ record = @klass.new
142
+ record.stub :artist_id_was => -1
143
+ record.should_not be_valid
144
+ end
145
+
146
+ it 'should uncache a stale association before validating' do
147
+ @klass = disposable_song_class do
148
+ assignable_values_for :artist do
149
+ [] # This example doesn't care about what's assignable. We're only interested in behavior up to the validation.
150
+ end
151
+ end
152
+ association = Artist.create!
153
+ record = @klass.new
154
+ record.stub(:artist => association, :artist_id => -1) # This is a stale association: The associated object's id doesn't match the foreign key. This can happen in Rails 2, not Rails 3.
155
+ record.should_receive(:artist).ordered.and_return(association)
156
+ record.should_receive(:artist).ordered.with(true).and_return(association)
157
+ record.valid?
158
+ end
159
+
160
+ it 'should not uncache a fresh association before validating' do
161
+ @klass = disposable_song_class do
162
+ assignable_values_for :artist do
163
+ [] # This example doesn't care about what's assignable. We're only interested in behavior up to the validation.
164
+ end
165
+ end
166
+ association = Artist.create!
167
+ record = @klass.new
168
+ record.stub(:artist => association, :artist_id => association.id) # This is a fresh association: The associated object's id matches the foreign key.
169
+ record.should_receive(:artist).with(no_args).and_return(association)
170
+ record.valid?
171
+ end
172
+
173
+ end
174
+
175
+ context 'when delegating using the :through option' do
176
+
177
+ it 'should obtain allowed values from a method with the given name' do
178
+ @klass = disposable_song_class do
179
+ assignable_values_for :genre, :through => :delegate
180
+ def delegate
181
+ OpenStruct.new(:assignable_song_genres => %w[pop rock])
182
+ end
183
+ end
184
+ @klass.new(:genre => 'pop').should be_valid
185
+ @klass.new(:genre => 'disallowed value').should_not be_valid
186
+ end
187
+
188
+ it 'should be able to delegate to a lambda, which is evaluated in the context of the record instance' do
189
+ @klass = disposable_song_class do
190
+ assignable_values_for :genre, :through => lambda { delegate }
191
+ def delegate
192
+ OpenStruct.new(:assignable_song_genres => %w[pop rock])
193
+ end
194
+ end
195
+ @klass.new(:genre => 'pop').should be_valid
196
+ @klass.new(:genre => 'disallowed value').should_not be_valid
197
+ end
198
+
199
+ it 'should skip the validation if that method returns nil' do
200
+ @klass = disposable_song_class do
201
+ assignable_values_for :genre, :through => :delegate
202
+ def delegate
203
+ nil
204
+ end
205
+ end
206
+ @klass.new(:genre => 'pop').should be_valid
207
+ end
208
+
209
+ end
210
+
211
+ context 'with :default option' do
212
+
213
+ it 'should allow to set a default' do
214
+ @klass = disposable_song_class do
215
+ assignable_values_for :genre, :default => 'pop' do
216
+ %w[pop rock]
217
+ end
218
+ end
219
+ @klass.new.genre.should == 'pop'
220
+ end
221
+
222
+ it 'should allow to set a default through a lambda' do
223
+ @klass = disposable_song_class do
224
+ assignable_values_for :genre, :default => lambda { 'pop' } do
225
+ %w[pop rock]
226
+ end
227
+ end
228
+ @klass.new.genre.should == 'pop'
229
+ end
230
+
231
+ it 'should evaluate a lambda default in the context of the record instance' do
232
+ @klass = disposable_song_class do
233
+ assignable_values_for :genre, :default => lambda { default_genre } do
234
+ %w[pop rock]
235
+ end
236
+ def default_genre
237
+ 'pop'
238
+ end
239
+ end
240
+ @klass.new.genre.should == 'pop'
241
+ end
242
+
243
+ end
244
+
245
+ context 'when generating methods to list assignable values' do
246
+
247
+ it 'should generate an instance method returning a list of assignable values' do
248
+ @klass = disposable_song_class do
249
+ assignable_values_for :genre do
250
+ %w[pop rock]
251
+ end
252
+ end
253
+ @klass.new.assignable_genres.should == %w[pop rock]
254
+ end
255
+
256
+ it "should define a method #human on strings in that list, which return up the value's' translation" do
257
+ @klass = disposable_song_class do
258
+ assignable_values_for :genre do
259
+ %w[pop rock]
260
+ end
261
+ end
262
+ @klass.new.assignable_genres.collect(&:human).should == ['Pop music', 'Rock music']
263
+ end
264
+
265
+ it 'should use String#humanize as a default translation' do
266
+ @klass = disposable_song_class do
267
+ assignable_values_for :genre do
268
+ %w[electronic]
269
+ end
270
+ end
271
+ @klass.new.assignable_genres.collect(&:human).should == ['Electronic']
272
+ end
273
+
274
+ it 'should not define a method #human on values that are not strings' do
275
+ @klass = disposable_song_class do
276
+ assignable_values_for :year do
277
+ [1999, 2000, 2001]
278
+ end
279
+ end
280
+ years = @klass.new.assignable_years
281
+ years.should == [1999, 2000, 2001]
282
+ years.first.should_not respond_to(:human)
283
+ end
284
+
285
+ it 'should call #to_a on the list of assignable values, allowing ranges and scopes to be passed as allowed value descriptors' do
286
+ @klass = disposable_song_class do
287
+ assignable_values_for :year do
288
+ 1999..2001
289
+ end
290
+ end
291
+ @klass.new.assignable_years.should == [1999, 2000, 2001]
292
+ end
293
+
294
+ it 'should evaluate the value block in the context of the record instance' do
295
+ @klass = disposable_song_class do
296
+ assignable_values_for :genre do
297
+ genres
298
+ end
299
+ def genres
300
+ %w[pop rock]
301
+ end
302
+ end
303
+ @klass.new.assignable_genres.should == %w[pop rock]
304
+ end
305
+
306
+ it 'should include a previously saved value, even if is no longer allowed' do
307
+ @klass = disposable_song_class do
308
+ assignable_values_for :genre do
309
+ %w[pop rock]
310
+ end
311
+ end
312
+ record = @klass.new(:genre => 'ballad')
313
+ record.save!(:validate => false)
314
+ record.assignable_genres.should =~ %w[pop rock ballad]
315
+ end
316
+
317
+ context 'with :through option' do
318
+
319
+ it 'should retrieve assignable values from the given method' do
320
+ @klass = disposable_song_class do
321
+ assignable_values_for :genre, :through => :delegate
322
+ def delegate
323
+ @delegate ||= 'delegate'
324
+ end
325
+ end
326
+ record = @klass.new
327
+ record.delegate.should_receive(:assignable_song_genres).and_return %w[pop rock]
328
+ record.assignable_genres.should == %w[pop rock]
329
+ end
330
+
331
+ it "should pass the record to the given method if the delegate's query method takes an argument" do
332
+ delegate = Object.new
333
+ def delegate.assignable_song_genres(record)
334
+ record_received(record)
335
+ %w[pop rock]
336
+ end
337
+ @klass = disposable_song_class do
338
+ assignable_values_for :genre, :through => :delegate
339
+ define_method :delegate do
340
+ delegate
341
+ end
342
+ end
343
+ record = @klass.new
344
+ delegate.should_receive(:record_received).with(record)
345
+ record.assignable_genres.should == %w[pop rock]
346
+ end
347
+
348
+ it 'should raise an error if the given method returns nil' do
349
+ @klass = disposable_song_class do
350
+ assignable_values_for :genre, :through => :delegate
351
+ def delegate
352
+ nil
353
+ end
354
+ end
355
+ expect { @klass.new.assignable_genres }.to raise_error(AssignableValues::DelegateUnavailable)
356
+ end
357
+
358
+ end
359
+
360
+ end
361
+
362
+ end
363
+
364
+ #describe '.authorize_values_for' do
365
+ #
366
+ # it 'should be a shortcut for .assignable_values_for :attribute, :through => :power' do
367
+ # @klass = disposable_song_class
368
+ # @klass.should_receive(:assignable_values_for).with(:attribute, :option => 'option', :through => :power)
369
+ # @klass.class_eval do
370
+ # authorize_values_for :attribute, :option => 'option'
371
+ # end
372
+ # end
373
+ #
374
+ # it 'should generate a getter and setter for a @power field' do
375
+ # @klass = disposable_song_class do
376
+ # authorize_values_for :attribute
377
+ # end
378
+ # song = @klass.new
379
+ # song.should respond_to(:power)
380
+ # song.should respond_to(:power=)
381
+ # end
382
+ #
383
+ #end
384
+
385
+ end
@@ -0,0 +1,2 @@
1
+ --exclude "spec/*,gems/*"
2
+ --rails
@@ -0,0 +1,21 @@
1
+ $: << File.join(File.dirname(__FILE__), "/../lib" )
2
+
3
+ # Set the default environment to sqlite3's in_memory database
4
+ ENV['RAILS_ENV'] ||= 'in_memory'
5
+ ENV['RAILS_ROOT'] = 'app_root'
6
+
7
+ # Load the Rails environment and testing framework
8
+ require "#{File.dirname(__FILE__)}/app_root/config/environment"
9
+ require "#{File.dirname(__FILE__)}/../lib/assignable_values"
10
+ require 'rspec/rails'
11
+
12
+ # Undo changes to RAILS_ENV
13
+ silence_warnings {RAILS_ENV = ENV['RAILS_ENV']}
14
+
15
+ # Run the migrations
16
+ ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate")
17
+
18
+ RSpec.configure do |config|
19
+ config.use_transactional_fixtures = true
20
+ config.use_instantiated_fixtures = false
21
+ end
metadata ADDED
@@ -0,0 +1,165 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: assignable_values
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Henning Koch
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-02-20 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 5
29
+ segments:
30
+ - 3
31
+ - 1
32
+ version: "3.1"
33
+ name: rails
34
+ version_requirements: *id001
35
+ type: :development
36
+ - !ruby/object:Gem::Dependency
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ hash: 19
44
+ segments:
45
+ - 2
46
+ - 8
47
+ version: "2.8"
48
+ name: rspec
49
+ version_requirements: *id002
50
+ type: :development
51
+ - !ruby/object:Gem::Dependency
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ~>
57
+ - !ruby/object:Gem::Version
58
+ hash: 19
59
+ segments:
60
+ - 2
61
+ - 8
62
+ version: "2.8"
63
+ name: rspec-rails
64
+ version_requirements: *id003
65
+ type: :development
66
+ - !ruby/object:Gem::Dependency
67
+ prerelease: false
68
+ requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 3
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ name: sqlite3
78
+ version_requirements: *id004
79
+ type: :development
80
+ description: Restrict the values assignable to ActiveRecord attributes or associations. Or enums on steroids.
81
+ email: henning.koch@makandra.de
82
+ executables: []
83
+
84
+ extensions: []
85
+
86
+ extra_rdoc_files: []
87
+
88
+ files:
89
+ - .gitignore
90
+ - .rspec
91
+ - Gemfile
92
+ - README.md
93
+ - Rakefile
94
+ - assignable_values.gemspec
95
+ - lib/assignable_values.rb
96
+ - lib/assignable_values/active_record.rb
97
+ - lib/assignable_values/active_record/restriction/base.rb
98
+ - lib/assignable_values/active_record/restriction/belongs_to_association.rb
99
+ - lib/assignable_values/active_record/restriction/scalar_attribute.rb
100
+ - lib/assignable_values/errors.rb
101
+ - lib/assignable_values/version.rb
102
+ - spec/app_root/.gitignore
103
+ - spec/app_root/app/controllers/application_controller.rb
104
+ - spec/app_root/app/models/artist.rb
105
+ - spec/app_root/app/models/song.rb
106
+ - spec/app_root/config/application.rb
107
+ - spec/app_root/config/boot.rb
108
+ - spec/app_root/config/database.yml
109
+ - spec/app_root/config/environment.rb
110
+ - spec/app_root/config/environments/in_memory.rb
111
+ - spec/app_root/config/environments/mysql.rb
112
+ - spec/app_root/config/environments/postgresql.rb
113
+ - spec/app_root/config/environments/sqlite.rb
114
+ - spec/app_root/config/environments/sqlite3.rb
115
+ - spec/app_root/config/environments/test.rb
116
+ - spec/app_root/config/initializers/backtrace_silencers.rb
117
+ - spec/app_root/config/initializers/inflections.rb
118
+ - spec/app_root/config/initializers/mime_types.rb
119
+ - spec/app_root/config/initializers/secret_token.rb
120
+ - spec/app_root/config/initializers/session_store.rb
121
+ - spec/app_root/config/locales/en.yml
122
+ - spec/app_root/config/routes.rb
123
+ - spec/app_root/db/migrate/001_create_artists.rb
124
+ - spec/app_root/db/migrate/002_create_songs.rb
125
+ - spec/app_root/lib/tasks/.gitkeep
126
+ - spec/app_root/script/rails
127
+ - spec/assignable_values/active_record_spec.rb
128
+ - spec/rcov.opts
129
+ - spec/spec_helper.rb
130
+ has_rdoc: true
131
+ homepage: https://github.com/makandra/assignable_values
132
+ licenses: []
133
+
134
+ post_install_message:
135
+ rdoc_options: []
136
+
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ none: false
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ hash: 3
145
+ segments:
146
+ - 0
147
+ version: "0"
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ hash: 3
154
+ segments:
155
+ - 0
156
+ version: "0"
157
+ requirements: []
158
+
159
+ rubyforge_project:
160
+ rubygems_version: 1.3.9.3
161
+ signing_key:
162
+ specification_version: 3
163
+ summary: Restrict the values assignable to ActiveRecord attributes or associations. Or enums on steroids.
164
+ test_files: []
165
+