diff --git a/README.md b/README.md index 941056b..acdaa00 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,20 @@ Restricting scalar attributes The basic usage to restrict the values assignable to strings, integers, etc. is this: - class Song < ActiveRecord::Base - assignable_values_for :genre do - ['pop', 'rock', 'electronic'] - end - end +```rb +class Song < ActiveRecord::Base + assignable_values_for :genre do + ['pop', 'rock', 'electronic'] + end +end +``` The assigned value is checked during validation: - Song.new(genre: 'rock').valid? # => true - Song.new(genre: 'elephant').valid? # => false +```rb +Song.new(genre: 'rock').valid? # => true +Song.new(genre: 'elephant').valid? # => false +``` The validation error message is the same as the one from `validates_inclusion_of` (`errors.messages.inclusion` in your I18n dictionary). You can also set a custom error message with the `:message` option. @@ -31,13 +35,15 @@ You can also set a custom error message with the `:message` option. ### Listing assignable values You can ask a record for a list of values that can be assigned to an attribute: - - song.assignable_genres # => ['pop', 'rock', 'electronic'] +```rb +song.assignable_genres # => ['pop', 'rock', 'electronic'] +``` This is useful for populating `` tag with pairs of internal values and human labels is to use the `collection_select` helper from Rails: - form.collection_select :genre, form.object.humanized_assignable_genres, :value, :humanized +```rb +form.collection_select :genre, form.object.humanized_assignable_genres, :value, :humanized +``` #### Humanized labels and inheritance For models that inherit assignable values you can override the humanized labels: - class FunnySong < Song - ... - end +```rb +class FunnySong < Song + ... +end +``` - en: - assignable_values: - funny_song: - genre: - pop: 'The stuff you hear on mainstream radio all day long' - rock: 'A lot of electric guitars and drums' - electronic: 'Whatever David Guetta does' +```yaml +en: + assignable_values: + funny_song: + genre: + pop: 'The stuff you hear on mainstream radio all day long' + rock: 'A lot of electric guitars and drums' + electronic: 'Whatever David Guetta does' +``` If no humanization is provided for the child model (i.e. the `funny_song.genre` key) humanization will fall back to the parent model (`song`). @@ -96,38 +116,45 @@ parent model (`song`). You can define a default value by using the `:default` option: - class Song < ActiveRecord::Base - assignable_values_for :genre, default: 'rock' do - ['pop', 'rock', 'electronic'] - end - end +```rb +class Song < ActiveRecord::Base + assignable_values_for :genre, default: 'rock' do + ['pop', 'rock', 'electronic'] + end +end +``` The default is applied to new records: - - Song.new.genre # => 'rock' +```rb +Song.new.genre # => 'rock' +``` Defaults can be procs: - class Song < ActiveRecord::Base - assignable_values_for :year, default: proc { Date.today.year } do - 1980 .. 2011 - end - end +```rb +class Song < ActiveRecord::Base + assignable_values_for :year, default: proc { Date.today.year } do + 1980 .. 2011 + end +end +``` The proc will be evaluated in the context of the record instance. You can also default a secondary default that is only set if the primary default value is not assignable: - - class Song < ActiveRecord::Base - assignable_values_for :year, default: 1999, secondary_default: proc { Date.today.year } do - (Date.today.year - 2) .. Date.today.year - end - end +```rb +class Song < ActiveRecord::Base + assignable_values_for :year, default: 1999, secondary_default: proc { Date.today.year } do + (Date.today.year - 2) .. Date.today.year + end +end +``` If called in 2013 the code above will fall back to: - Song.new.year # => 2013 - +```rb +Song.new.year # => 2013 +``` ### Allowing blank values @@ -136,11 +163,13 @@ will get a validation error. If you would like to change this behavior and allow blank values to be valid, use the `:allow_blank` option: - class Song < ActiveRecord::Base - assignable_values_for :genre, default: 'rock', allow_blank: true do - ['pop', 'rock', 'electronic'] - end - end +```rb +class Song < ActiveRecord::Base + assignable_values_for :genre, default: 'rock', allow_blank: true do + ['pop', 'rock', 'electronic'] + end +end +``` The `:allow_blank` option can be a symbol, in which case a method of that name will be called on the record. @@ -151,34 +180,44 @@ The `:allow_blank` option can also be a proc, in which case the proc will be cal Values are only validated when they change. This is useful when the list of assignable values can change during runtime: - class Song < ActiveRecord::Base - assignable_values_for :year do - (Date.today.year - 2) .. Date.today.year - end - end +```rb +class Song < ActiveRecord::Base + assignable_values_for :year do + (Date.today.year - 2) .. Date.today.year + end +end +``` If a value has been saved before, it will remain valid, even if it is no longer assignable: - Song.update_all(year: 1985) # update all records with a value that is no longer valid - song = Song.last - song.year # => 1985 - song.valid? # => true +```rb +Song.update_all(year: 1985) # update all records with a value that is no longer valid +song = Song.last +song.year # => 1985 +song.valid? # => true +``` It will also be returned when obtaining the list of assignable values: - song.assignable_years # => [2010, 2011, 2012, 1985] +```rb +song.assignable_years # => [2010, 2011, 2012, 1985] +``` However, if you want only those values that are actually intended to be assignable, e.g. when updating a `` menus with blank selections when opening an old record in a web form. @@ -188,7 +227,7 @@ Assignable values can also be used for array values. This works when you use Rai To validate array values, pass `multiple: true`: -``` +```rb class Song < ActiveRecord::Base serialize :genres # skip this when you use PostgreSQL and an array type column @@ -202,7 +241,7 @@ In this case, every *subset* of the given values is valid, for example `['pop', For humanization, you can still use -``` +```rb song.humanized_genre('pop') # => "Pop music" song.humanized_assignable_genres.last.humanized # => "Electronic music" ``` @@ -213,30 +252,34 @@ Restricting belongs_to associations You can restrict `belongs_to` associations in the same manner as scalar attributes: - class Song < ActiveRecord::Base +```rb +class Song < ActiveRecord::Base - belongs_to :artist + belongs_to :artist - assignable_values_for :artist do - Artist.where(signed: true) - end + assignable_values_for :artist do + Artist.where(signed: true) + end - end +end +``` Listing and validating also works the same: - chicane = Artist.create!(name: 'Chicane', signed: true) - lt2 = Artist.create!(name: 'LT2', signed: false) +```rb +chicane = Artist.create!(name: 'Chicane', signed: true) +lt2 = Artist.create!(name: 'LT2', signed: false) - song = Song.new +song = Song.new - song.assignable_artists # => [#] +song.assignable_artists # => [#] - song.artist = chicane - song.valid? # => true +song.artist = chicane +song.valid? # => true - song.artist = lt2 - song.valid? # => false +song.artist = lt2 +song.valid? # => false +``` Similiar to scalar attributes, associations are only validated when the foreign key (`artist_id` in the example above) changes. Values stored in the database will remain assignable until they are changed, and you can query actually assignable values with `song.assignable_artists(include_old_value: false)`. @@ -249,18 +292,20 @@ How assignable values are evaluated 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: - class Song < ActiveRecord::Base +```rb +class Song < ActiveRecord::Base - validates_numericality_of :year + validates_numericality_of :year - assignable_values_for :genre do - genres = [] - genres << 'jazz' if year > 1900 - genres << 'rock' if year > 1960 - genres - end + assignable_values_for :genre do + genres = [] + genres << 'jazz' if year > 1900 + genres << 'rock' if year > 1960 + genres + end - end +end +``` Obtaining assignable values from another source @@ -270,54 +315,62 @@ The list of assignable values can be provided by any object that is accessible f You can define the source of assignable values by setting the `:through` option to a proc: - class Story < ActiveRecord::Base - assignable_values_for :state, through: proc { Power.current } - end +```rb +class Story < ActiveRecord::Base + assignable_values_for :state, through: proc { Power.current } +end +``` `Power.current` must now respond to a method `assignable_story_states` or `assignable_story_states(story)` which returns an `Enumerable` of state strings: - class Power +```rb +class Power - cattr_accessor :current + cattr_accessor :current - def initialize(role) - @role = role - end + def initialize(role) + @role = role + end - def assignable_story_states(story) - states = ['draft', 'pending'] - states << 'accepted' if @role == :admin - states - end + def assignable_story_states(story) + states = ['draft', 'pending'] + states << 'accepted' if @role == :admin + states + end - end +end +``` Listing and validating works the same with delegation: +```rb +story = Story.new(state: 'accepted') - story = Story.new(state: 'accepted') - - Power.current = Power.new(:guest) - story.assignable_states # => ['draft', 'pending'] - story.valid? # => false +Power.current = Power.new(:guest) +story.assignable_states # => ['draft', 'pending'] +story.valid? # => false - Power.current = Power.new(:admin) - story.assignable_states # => ['draft', 'pending', 'accepted'] - story.valid? # => true +Power.current = Power.new(:admin) +story.assignable_states # => ['draft', 'pending', 'accepted'] +story.valid? # => true +``` Note that delegated validation is skipped when the delegate is `nil`. This way your model remains usable when there is no authorization context, like in batch processes or the console: - - story = Story.new(state: 'foo') - Power.current = nil - story.valid? # => true +```rb +story = Story.new(state: 'foo') +Power.current = nil +story.valid? # => true +``` Think of this as enabling an optional authorization layer on top of your model validations, which can be switched on or off depending on the current context. Instead of a proc you can also use the `:through` option to name an instance method: - class Story < ActiveRecord::Base - attr_accessor :power - assignable_values_for :state, through: :power - end +```rb +class Story < ActiveRecord::Base + attr_accessor :power + assignable_values_for :state, through: :power +end +``` ### Obtaining assignable values from a Consul power @@ -326,28 +379,34 @@ A common use case for the `:through` option is when there is some globally acces If you are using [Consul](https://github.com/makandra/consul), you will get a lot of this plumbing for free. Consul gives you a macro `current_power` to instantiate a so called "power", which describes what the current user may access: - class ApplicationController < ActionController::Base - include Consul::Controller +```rb +class ApplicationController < ActionController::Base + include Consul::Controller - current_power do - Power.new(current_user) - end + current_power do + Power.new(current_user) + end - end +end +``` The code above will provide you with a helper method `current_power` for your controller and views. Everywhere else, you can simply access it from `Power.current`. You can now delegate validation of assignable values to the current power by saying: - class Story < ActiveRecord::Base - authorize_values_for :state - end +```rb + class Story < ActiveRecord::Base + authorize_values_for :state + end +``` This is a shortcut for saying: - class Story < ActiveRecord::Base - assignable_values_for :state, through: proc { Power.current } - end +```rb + class Story < ActiveRecord::Base + assignable_values_for :state, through: proc { Power.current } + end +``` Head over to the [Consul README](https://github.com/makandra/consul) for details. @@ -356,8 +415,9 @@ Installation ------------ Put this into your `Gemfile`: - - gem 'assignable_values' +```rb +gem 'assignable_values' +``` Now run `bundle install` and restart your server. Done.