Usage of Attribute Filters
Attribute sets
Attribute set is a set of attribute names and optional annotations. It's like a hash and internally it is a kind of Hash, but differs in many places.
Attribute sets have simple function; they group attribute names. What can you do with that? For example you can use it to perform some tasks on all attribute names that are stored in a set (or their values). You can also combine sets, intersect, exclusively disjuct them, merge with other data, query them, iterate through them, and so on.
Data structures
Attribute sets are instances of
ActiveModel::AttributeSet
class. You can create and update sets freely and store in them wherever you want.
For your convenience Attribute Filters also provides globally created attribute sets that are bound to your models (at the class-level). The binding is realized simply by using the hash of all these sets. The attribute sets assigned to models are stored as class instance variable.
You should interact with the global sets using dedicated DSL keywords that are available in your models. These methods will allow you to read or write some data from/to global attribute sets.
Attribute Filters are using AttributeSet
instances not just to store internal data
but also to interact with other parts of a program. So whenever there is a method
that returns a set of attributes or even a set of set names, the returned value
will probably be an instance of the AttributeSet
class.
Note that when sets are returned the convention is that:
- attribute names are strings
- set names are symbols
- annotations are hashes attached to attribute names (strings)
- annotation keys are symbols
Once you create some set that is bound to your model (there is a class method attribute_set
for it)
you cannot remove elements from it. Moreover, any query returning its contents will always
give you the deep copy. Such a separation is a consequence of the idea that the model-bound attribute
sets should be a part of the interface, which shouldn't change at runtime. This approach
enforces the declarative design (which is very concise and clear as you read the code).
Defining the model-level attribute sets
First thing that should be done when using the Attribute Filters is defining the sets of attributes in models.
You can also create local sets (using ActiveModel::AttributeSet.new
)
or a local sets with extra syntactic sugar (using ActiveModel::AttributeSet::Query.new
)
but in real-life scenarios you will use model-bound sets that can be later used
by common filters and by your own methods.
attribute_set(set_name => attr_names)
The attribute_set
(a.k.a has_attributes_that
) class method allows to create or update a set that will be
tied to a model.
Example:
class User < ActiveRecord::Base
has_attributes_that should_be_stripped: [ :username, :email, :real_name ]
has_attributes_that should_be_downcased: [ :username, :email ]
has_attributes_that should_be_capitalized: [ :real_name ]
end
Instead of attribute_set
you may also use one of the aliases:
af_attribute_set
,attributes_set
,attributes_that_are
,attributes_that
,properties_that
,
has_attribute_set
,has_attribute_that
,has_attribute_that_is
,has_attributes
,
has_attributes_set
,has_attributes_that_are
,has_attributes_that
,has_properties_that
WARNING
If you are using other module that provides attribute_set
class method (e.g. Virtus) then make sure you are including AttributeFilters
(or ActiveModel::AttributeMethods
) after including that module. Then use any other alias
(e.g. af_attribute_set
) instead of attribute_set
.
filter_attribute(attr => set_names)
You may prefer the alternative syntax, that uses attribute names
as primary arguments, but has exactly the same effect as the attribute_set
.
The filter_attribute
(a.k.a the_attribute
) class method also lets you create or update a set, just the arguments order is reversed
(attribute name goes first).
Example:
class User < ActiveRecord::Base
its_attribute real_name: [ :should_be_stripped, :should_be_capitalized ]
its_attribute username: [ :should_be_stripped, :should_be_downcased ]
its_attribute email: [ :should_be_stripped, :should_be_downcased ]
end
Instead of filter_attribute
you may also use one of the aliases:
the_attribute
,attribute_to_set
,filtered_attribute
,filtered_attributes
,filters_attribute
,filters_attributes
,its_attribute
,has_attribute
,has_the_attribute
,has_filtered_attribute
,has_filtered_attributes
Mixing the syntaxes
You can mix both syntaxes given in the examples before. You can also use regular arguments instead of hashes to create sets:
class User < ActiveRecord::Base
has_attributes_that :should_be_stripped, :username, :email, :real_name
its_attribute :real_name, :should_be_capitalized
its_attribute :username, :should_be_downcased
its_attribute :email, :should_be_downcased
end
Querying sets in models
When your "global" attribute sets are defined, you can use couple of class methods to query them.
attribute_set(set_name)
The attribute_set
(a.k.a attributes_that
) class method called with a single argument returns the attribute set
of the given name.
It will always return the instance of AttributeSet
class, even if there is no set registered under
the given name (in that case the resulting set will be empty). To know if the returned set is a stub
(placed instead of the missing one) you can use frozen?
on it or you can call the DSL method
attribute_set_exists?(set_name)
before querying the global sets for a model.
Example:
User.attributes_that(:should_be_stripped) - User.attributes_that(:should_be_downcased)
# => #<ActiveModel::AttributeSet: {"real_name"}>
Note that the returned set will be a copy of the original set stored within your model.
Alternative syntax:
User.attributes_that.should_be_stripped - User.attributes_that.should_be_downcased
# => #<ActiveModel::AttributeSet: {"real_name"}>
Instead of attribute_set
you may also use one of the aliases:
attributes_set
,attributes_that_are
,attributes_that
,properties_that
,
has_attribute_set
,has_attribute_that
,has_attribute_that_is
,has_attributes
,
has_attributes_set
,has_attributes_that_are
,has_attributes_that
,has_properties_that
attribute_sets
The attribute_sets
class method returns a MetaSet kind of value containing all the defined attribute sets indexed by their names.
Example:
User.attribute_sets.keys
# => [:should_be_downcased, :should_be_stripped, :should_be_capitalized]
User.attribute_sets.first
# => [:should_be_downcased, #<ActiveModel::AttributeSet: {"username", "email"}>]
Note that the returned hash will have a default value set to instance of the AttributeSet
.
If you'll try to get the value of an element that doesn't exist the empty set will be returned.
It won't return the exact internal hash but a duplicate.
Instead of attribute_sets
you may also use one of the aliases:
attributes_sets
,properties_sets
Note that the returned sets will be copies of the original sets stored within your model.
attribute_set
The attribute_set
(a.k.a attributes_that
) class method called without any arguments is a DSL wrapper that calls attribute_set(set_name)
with set_name
taken from next method call.
Example:
User.attributes_that.should_be_downcased
# => #<ActiveModel::AttributeSet: {"username", "email"}>
filter_attribute(attribute_name)
The filter_attribute
(a.k.a the_attribute
) class method called with a single argument
is used for checking what are the sets that the attribute belongs to.
It will always return an AttributeSet
instance, even if there is no attribute of the given name
(in that case the resulting set will be empty).
Example:
User.the_attribute :real_name
# => #<ActiveModel::AttributeSet: {:should_be_stripped, :should_be_capitalized }>
Instead of filter_attribute
you may also use one of the aliases:
filtered_attribute
,the_attribute
,add_attribute_to_set
,
add_attribute_to_sets
,attribute_to_set
,filtered_attributes
attributes_to_sets
The attributes_to_sets
class method returns a MetaSet kind of value containing all filtered attributes and sets that the attributes
belong to. The meta set is indexed by attribute names.
Example:
User.attributes_to_sets.keys
# => ["real_name", "username", "email"]
Note that the returned hash will have a default value set to instance of the AttributeSet
.
If you'll try to get the value of an element that doesn't exist the empty set will be returned.
Instead of attributes_to_sets
you may also use one of the aliases:
attribute_sets_map
filter_attribute
The filter_attribute
(a.k.a the_attribute
) class method called without any arguments
is a wrapper that calls attributes_to_sets
which returns
a returns a MetaSet kind of value containing all filtered attributes and sets that the attributes
belong to.
Querying sets in model objects
It is possible to access attribute sets from within the ActiveModel (or ORM, like ActiveRecord) objects. To do that you may use instance methods that are designed for that purpose.
attribute_set
The attribute_set
instance method called withount an argument returns the attribute set object containing all attributes known
in a current model.
Example:
User.first.attribute_set
# => #<ActiveModel::AttributeSet: {"id", "username", "email", "password", "language", "created_at", "updated_at"}>
It works the same as the all_attributes
method.
WARNING
If you are using other module that provides attribute_set
method (e.g. Virtus) then make sure you are including AttributeFilters
(or ActiveModel::AttributeMethods
) after including that module. Then use any other alias
(e.g. af_attribute_set
) instead of attribute_set
.
attribute_set(set_name)
The attribute_set
(a.k.a attributes_that
) instance method called with a single argument returns a copy of the attribute set
of the given name. It won't return the exact set object but a duplicate.
It will always return an AttributeSet
instance, even if there is no set of the given name defined for a model
(in that case the resulting set will be empty).
Example:
User.first.attributes_that(:should_be_stripped)
# => #<ActiveModel::AttributeSet: {"real_name", "username", "email"}>
The returned object in transparently wrapped in a proxy class instance that allows you to apply additional methods and keywords to it. See the section 'Syntactic sugar for queries' for more info.
There are also variants of this method that differ in a kind of taken argument:
attribute_set(set_object)
Allows to wrap an existing attribute set instance (e.g. created locally) with transparent proxy instance. The resulting object will look like the same set but will be decorated with additional syntactic sugar.
attribute_set(any_object)
Allows to create local set that will be initialized with the given object (usually an array) that may not be a
String
, aSymbol
or anAttributeSet
(these are reserved for the variants above). The resulting object (a newAttributeSet
instance) is also wrapped in a proxy.
Instead of attribute_set
you may also use one of the aliases:
af_attribute_set
,attributes_set
,attributes_that_are
,attributes_that
,properties_that
,
has_attribute_set
,has_attribute_that
,has_attribute_that_is
,has_attributes
,
has_attributes_set
,has_attributes_that_are
,has_attributes_that
,has_properties_that
Instead of attribute_set
you may also use:
attribute_set_simple(set_name)
It works same way as attribute_set(set_name)
but doesn't wrap the result in a transparent proxy
object that brings some syntactic sugar (explained later).
WARNING
If you are using other module that provides attribute_set
method (e.g. Virtus) then make sure you are including AttributeFilters
(or ActiveModel::AttributeMethods
) after including that module. Then use any other alias
(e.g. af_attribute_set
) instead of attribute_set
.
attribute_sets
The attribute_sets
method returns a meta set containing all the defined attribute sets indexed by their names.
It won't return the exact internal hash but a duplicate and every set within it will also be a duplicate
of the original one.
Example:
User.first.attribute_sets.keys
# => [:should_be_downcased, :should_be_stripped, :should_be_capitalized]
User.first.attribute_sets.first
# => [:should_be_downcased, #<ActiveModel::AttributeSet: {"username", "email"}>]
Note that the returned hash will have a default value set to instance of the AttributeSet
.
If you'll try to get the value of an element that doesn't exist the empty, frozen set will be returned.
Instead of attribute_sets
you may also use one of the aliases:
attributes_sets
,properties_sets
all_attributes
The all_attributes
method returns the attribute set containing all known attributes. It combines many other sets,
including the sets containing attributes marked as semi-real and virtual (explained later)
and temporary sets obtained by calling various Rails methods. It just tries to collect as many
known attribute names as possible.
Example:
User.first.all_attributes
# => #<ActiveModel::AttributeSet: {"id", "username", "email", "password", "language", "created_at", "updated_at"}>
Be aware that this method requires that the used ORM has attributes
method available for a model object.
Instead of all_attributes
you may also use the alias:
all_attributes_set
or call the instance method attribute_set
without arguments.
The all_attributes
synopsis is really:
all_attributes(simple = false, no_presence_check = true)
First argument (simple
) causes the method to not wrap the result in a transparent proxy
object that brings some syntactic sugar (explained later).
Second argument (no_presence_check
) causes the method to not
check if each attribute exists by verifying presence of its accessor
in case of semi-real attributes set that is merged into the resulting set.
all_accessible_attributes
The all_accessible_attributes
method returns the attribute set containing all accessible attributes.
Example:
User.first.all_accessible_attributes
# => #<ActiveModel::AttributeSet: {"username", "email", "language"}>
Be aware that this method requires that the used ORM has accessible_attributes
method available for a model
class. This method works only if your Rails application supoorts accessible attributes (versions up to 3 support it).
Instead of all_accessible_attributes
you may also use the alias:
accessible_attributes_set
Instead of all_accessible_attributes
you may also use:
all_accessible_attributes(true)
It works the same way but doesn't wrap the result in a transparent proxy object that brings some syntactic sugar (explained later).
all_inaccessible_attributes
The all_inaccessible_attributes
method returns the attribute set containing all inaccessible attributes.
Example:
User.first.all_inaccessible_attributes
# => #<ActiveModel::AttributeSet: {"id", "confirmation_token", "encrypted_password", "deleted_at"}>
Be aware that this method requires that the used ORM has accessible_attributes
and protected_attributes
method available in a model. This method works only if your Rails application supoorts accessible
attributes (versions up to 3 support it).
Instead of all_inaccessible_attributes
you may also use the alias:
inaccessible_attributes_set
Instead of all_inaccessible_attributes
you may also use:
all_inaccessible_attributes(true)
It works the same way but doesn't wrap the result in a transparent proxy object that brings some syntactic sugar (explained later).
The all_inaccessible_attributes
synopsis is really:
all_inaccessible_attributes(simple = false, no_presence_check = true)
First argument (simple
) causes the method to not wrap the result in a transparent proxy
object that brings some syntactic sugar (explained later).
Second argument (no_presence_check
) causes the method to not
check if each attribute exists by verifying presence of its accessor
in case of semi-real attributes set that is used to compute the resulting set.
all_protected_attributes
The all_protected_attributes
method returns the attribute set containing all protected attributes.
Example:
User.first.all_protected_attributes
# => #<ActiveModel::AttributeSet: {"id", "type"}>
Be aware that this method requires that the used ORM has protected_attributes
method available for a model
class. This method works only if your Rails application supoorts accessible attributes (versions up to 3 support it).
Instead of all_protected_attributes
you may also use the alias:
protected_attributes_set
Instead of all_protected_attributes
you may also use:
all_protected_attributes(true)
It works the same way but doesn't wrap the result in a transparent proxy object that brings some syntactic sugar (explained later).
all_semi_real_attributes
The all_semi_real_attributes
method returns the attribute set containing all attributes marked as semi-real. This is the concept
of Attribute Filters used to handle attributes that aren't stored in a database (a.k.a virtual attributes, explained later).
Example:
class User
treats_as_real :trololo
end
User.first.all_semi_real_attributes
# => #<ActiveModel::AttributeSet: {"trololo"}>
Instead of all_semi_real_attributes
you may also use one of the aliases:
semi_real_attributes_set
treat_as_real
(without arguments)
The all_semi_real_attributes
synopsis is really:
all_semi_real_attributes(simple = false, no_presence_check = true)
First argument (simple
) causes the method to not wrap the result in a transparent proxy
object that brings some syntactic sugar (explained later).
Second argument (no_presence_check
) causes the method to not
check if each attribute exists in order to include its name into set.
all_virtual_attributes
The all_virtual_attributes
method returns the attribute set containing all virtual attributes.
Example:
class User
attr_virtual :lol
end
User.first.all_virtual_attributes
# => #<ActiveModel::AttributeSet: {"lol"}>
You can check which virtual attribute is automatically tracked for changes (was added using attr_virtual
and has wrapped setter method), which is still waiting for its setter and getter methods to be created,
and which was added using treat_as_virtual
with the assumption that the changes tracking will be done
manually by the properly constructed setter.
Each element of a set containing virtual attributes has a control marker attached to it (instead of annotations) so you can read this marker to know what is the status of a certain attribute.
To get a hash of attributes and statuses pairs you may use (in an instance method that belongs to a model):
all_virtual_attributes.to_hash
or just print them:
all_virtual_attributes.each_pair{ |k,v| puts "#{k}: #{v}" }
To read the status of a single attribute:
all_virtual_attributes["attribute_name"]
To select the attributes that are meeting certain criteria:
all_virtual_attributes.select{|k,v| v == :tracked}
end
The statuses are symbols and have the following meanings:
:no_wrap
– a virtual attribute was added usingtreat_as_virtual
and changes tracking should be done manually:tracked
– a setter method of an attribute was automatically wrapped to track changes:waiting
– a virtual attribute is to be tracked when its setter method will be defined
Instead of all_virtual_attributes
you may also use the alias:
virtual_attributes_set
Instead of all_virtual_attributes
you may also use:
all_virtual_attributes(true)
It works the same way but doesn't wrap the result in a transparent proxy object that brings some syntactic sugar (explained later).
filtered_attribute(attribute_name)
The filtered_attribute
(a.k.a the_attribute
) method called with a single argument is used for checking
what are the sets that the attribute belongs to. It won't return the exact set object but a duplicate.
It will always return AttributeSet object, even if there is no attribute of the given name
(in that case the resulting set will be empty and frozen).
If the attribute name is not given or it's +nil+ then the name of an attribute is taken from the name of a next method in the chain call.
Example:
User.first.the_attribute :real_name
# => #<ActiveModel::AttributeSet: {:should_be_stripped, :should_be_capitalized }>
Instead of filtered_attribute
you may also use one of the aliases:
the_attribute
,is_the_attribute
,are_attributes
,
are_the_attributes
filtered_attribute_simple(attribute_name)
The filtered_attribute_simple
method is a version of `filtered_attribute
method that doesn't wrap the resulting
object in a proxy object. The attribute name must be given.
attributes_to_sets
The attributes_to_sets
method returns a meta set containing all filtered attributes and sets that the attributes belong to.
The meta set is indexed by attribute names.
Example:
User.first.attributes_to_sets.keys
# => ["real_name", "username", "email"]
Note that the returned hash will have a default value set to instance of the AttributeSet
.
It won't return the exact internal hash but a duplicate.
If you'll try to get the value of an element that doesn't exist the empty set will be returned.
Instead of attributes_to_sets
you may also use one of the aliases:
attribute_sets_map
valid?
and invalid?
Validation helpers are used to test if the attributes present in a set
are valid. When valid?
is called then all attributes from a set
must be valid for the method to return true
. When invalid?
is called
then any invalid attribute will cause the expression to return true
.
Be aware that calling one of these methods will run validation process
on an instance of a model. Besides, your ORM should have the errors
hash available in order to use it (Active Record has it).
Example:
u = User.first
u.attributes_that(:should_be_stripped).valid?
# => true
u.attributes_that(:should_be_stripped).invalid?
# => false
You can also use one of the aliases:
invalid?
,is_not_valid?
,any_invalid?
,are_not_valid?
,not_valid?
,is_any_invalid?
valid?
,is_valid?
,are_valid?
,all_valid?
,are_all_valid?
changed?
and unchanged?
The changed?
and unchanged?
helpers allow you to test if any attribute listed in the set
has changed its value (changed?
) or if all attributes from a set remain unchanged (unchanged?
).
u = User.first
u.username = ' '
# => " "
u.all_attributes.any_changed?
# => true
u.all_attributes.unchanged?
# => false
You can also use one of the aliases:
changed?
,any_changed?
,have_changed?
,have_any_changed?
,is_any_changed?
,has_any_changed?
,has_changed?
unchanged?
,none_changed?
,nothing_changed?
,is_unchanged?
,are_all_unchanged?
,all_unchanged?
,havent_changed?
,arent_changed?
,are_not_changed?
,none_changed?
,not_changed?
values
This method simply returns the values of all attributes in a set as an array.
to_set
This method simply produces an object that is a kind of Set and contains all attribute names.
AttributeSet
enumerators
The AttributeSet
class is derived from Hash
class. The keys are attributes,
the values are usually true
(TrueClass
) or hashes (Hash
) if there are any annotations attached
to attribute name. All overriden enumerators are kind of AttributeSet::Enumerator
.
Let's see what are the differences from standard enumerators that can be found in the Hash class.
each
– works on keys and values (behaves likeeach_pair
) but usable with just one argument in a block (key)each_pair
– iterates throught key => value pairseach_name_value(active_model_object, no_presence_check = false)
– iterates throught key => attribute value pairscollect
(map
) – produces array containg keys that may be altered by the given blockselect
– selects new hash with elements for which the given block evaluates to +true+ (yields: keys, values)select_accessible(active_model_object)
– same asselect
but picks the attributes which have accessorsreject
– reversedselect
sort
– sorts AttributeSet object by keyssort_by
– sorts AttributeSet object using the comparison method from a block
Syntactic sugar for queries
Querying attribute sets may be even more sweet when you add some syntactic sugar provided by the Attribute Filters. The methods used for querying make use of internal proxy classes that provide additional DSL keywords that can be used to express the program logic. These are present when querying in an instance, not in a model class.
The additional features depend of query type and are different for querying atrribute sets and different for querying attributes for associated sets. In practice there are two groups of methods with two sets of DSL features.
First group (querying attribute sets):
attribute_set
and aliasesattribute_set(set_name)
and aliasesattributes_to_filter
Second group (querying attributes for sets they belong to):
filtered_attribute
and aliases
Querying attribute sets
Querying attribute sets uses two instance methods available in your models:
attribute_set
and aliasesattribute_set(set_name)
and aliasesattributes_to_filter
The output of calling these methods is an AttributeSet
instance
wrapped within a transparent proxy class instance
(AttributeSet::Query
).
Eager wrapping
Whenever an output generated by that methods
is a kind of AttributeSet
the result is wrapped again
in order to help you in creating nice looking method call chains like:
User.first.attributes_that(:should_be_stripped).sort.list.present?
Set & attribute names as methods
It's possible to enter a set name or an attribute name as a name
of next method in a call chain instead of passing them as a symbolic argument.
It works with DSL instance methods attribute_set(set_name)
and filtered_attribute(attribute_name)
.
Examples:
User.first.attributes_that.should_be_stripped # same as User.first.attributes_that(:should_be_stripped)
User.first.attributes_that_are.required_to_use_app.present?
User.first.attributes_that.should_be_stripped.sort.list.present?
User.first.the_attribute.username # same as User.first.the_attribute(:username)
User.first.the_attribute.username.list.sets
User.first.the_attribute.username.valid?
Neutral methods
are
is
be
should
You can attach "neutral" methods to the output of attribute_set
or attributes_to_filter
and the same object will be returned
(equivalent to self
).
Example:
User.first.attributes_that(:should_be_stripped).present?
# => true
User.first.attributes_that(:should_be_stripped).are.present?
# => true
Presence selectors
all
any
none
one
The example above was just the check to see if the returned set is empty or not. How about checking if all of the attributes that belong to a set are present (meaning their values)?
Let's do it without any fancy DSL methods:
u = User.first
u.attributes_that(:should_be_stripped).all? do |attribute_name|
u.public_send(attribute_name).present?
end
# => false
Now the same task but with some sugar:
User.first.attributes_that(:should_be_stripped).all.present?
# => false
How it works? Whenever all
, any
, one
or none
is called on an output of the set querying methods
the call is forwarded to the all?
, any?
, one?
or none?
method
with passed a block in which the next method given in chain is invoked
for a value of each attribute from a set. The additional arguments,
if any, are also forwarded and passed to the method called within a block.
Just imagine that:
attributes_that(:should_be_stripped).all.METHOD(ARGUMENTS)
becomes:
attributes_that(:should_be_stripped).all? { |attribute| attribute.METHOD(ARGUMENTS) }
Another example, but with any
:
User.first.attributes_that(:should_be_stripped).any.present?
# => true
You also do:
User.first.attributes_that(:should_be_stripped).any.changed?
# => false
Elements selectors
list
show
There are two more DSL methods: list
and show
(both doing the same thing).
They forward a call the same way as the presence selectors do but the called
method is not any?
or all?
but select
. For instance:
u = User.first
u.attributes_that(:should_be_stripped).list.present?
# => #<ActiveModel::AttributeSet: {"username", "email"}>
is equivalent to:
u = User.first
u.attributes_that(:should_be_stripped).select do |atr|
u.atr.present?
end
# => #<ActiveModel::AttributeSet: {"username", "email"}>
Validation helpers
valid?
invalid?
Syntactic sugar for validation helpers is used when calls to these methods are combined with selectors described above (presence selectors and elements selectors).
Examples:
u = User.first
u.attributes_that(:should_be_stripped).all.valid?
# => true
u.attributes_that(:should_be_stripped).list.valid?
# => #<ActiveModel::AttributeSet: {"username", "email"}>
User.first.attributes_that(:should_be_stripped).is.any.valid?
# => true
User.first.are_attributes_that(:should_be_stripped).all.valid?
# => true
Be aware that calling these methods causes model object to be validated. The required condition
to use these methods is the ORM that has errors
hash (Active Record has it).
Changes tracking helpers
changed?
unchanged?
Syntactic sugar for changes tracking is used when calls to these methods are combined with selectors described above (presence selectors and elements selectors).
Examples:
u = User.first
u.username = " "
u.attributes_that(:should_be_stripped).all.changed?
# => false
u.attributes_that(:should_be_stripped).any.changed?
# => true
u.attributes_that(:should_be_stripped).list.changed?
# => #<ActiveModel::AttributeSet: {"username"}>
User.first.all_attributes.is.any.unchanged?
# => true
User.first.all_attributes.are.none.unchanged?
# => false
Be aware that the required condition to use these methods is the ORM that
has changes
hash (Active Record has it).
Calling custom methods
It's possible to call any other methods using selectors. Note that te method next to the given selector will be called on a value of each tested attribute.
Examples:
u = User.first
u.attributes_that(:should_be_stripped).all.is_a?(String)
# => false
u.attributes_that(:should_be_stripped).any.is_a?(String)
# => false
u.attributes_that(:should_be_stripped).list.is_a?(String)
# => #<ActiveModel::AttributeSet: {"username", "email"}>
Querying attributes
Querying attributes to know sets they belong to uses one instance method available in your models:
filtered_attribute
and aliases
The output of calling this method is an AttributeSet
instance
containing attribute set names (instead of attribute names),
wrapped within a transparent proxy class instance
(AttributeSet::AttrQuery
).
Neutral methods
are
is
one
is_one
in
list
be
should
the
a
sets
in_set
in_sets
in_a_set
belongs_to
You can attach "neutral" methods to the output of filtered_attribute
(a.k.a the_attribute
) instance method and the same object will be returned
(equivalent to self
).
Example:
User.first.the_attribute(:username).list.sets
# => {:should_be_downcased => true, :should_be_stripped => true}
User.first.the_attribute(:username).list.sets.to_a
# => [:should_be_downcased, :should_be_stripped]
Attribute membership querying
belongs_to?
in?
in_set?
in_a_set?
in_the_set?
the_set?
set?
is_one_that?
one_that?
that?
These methods allow to test if the attribute belongs to the given
attribute set or sets. You may consider the methods above aliases
of the include?
. The one difference is that all the given arguments
are changed to symbols.
Example:
User.first.the_attribute(:username).is.in.set.that?(:should_be_stripped)
# => true
User.first.the_attribute(:username).belongs_to.set.that?(:should_be_stripped)
# => true
Attribute membership testing
Membership querying has different syntax from simple testing. It uses set name with the question mark attached to it.
Example:
User.first.the_attribute(:username).should_be_stripped?
# => true
To use that syntax you have to be sure that there is no already defined method for AttributeSet object which name ends with question mark. Otherwise you may get false positives or a strange errors when trying to test if attribute belongs to a set. The real method call will override your check.
Attribute accessibility testing
accessible?
inaccessible?
protected?
is_accessible?
is_inaccessible?
is_protected?
The methods above allow to you to test if certain attribute is accessible, inaccessible or protected.
Examples:
u = User.first
u.the_attribute(:id).is.accessible?
# => false
u.the_attribute(:id).is.protected?
# => true
u.the_attribute(:id).is.inaccessible?
# => true
Be aware that this method requires that the used ORM has accessible_attributes
and protected_attributes
methods
available in a model. This method works only if your Rails application supoorts accessible attributes (versions up to 3 support it).
Attribute validity testing
valid?
invalid?
is_valid?
is_invalid?
The methods above allow you to test if certain attribute that changed is valid or invalid.
Examples:
u = User.first
u.username = ' '
u.the_attribute(:username).is.valid?
# => false
u.the_attribute(:username).is_valid?
# => false
u.the_attribute(:username).invalid?
# => true
Be aware that this method requires that the used ORM has valid?
and errors
methods that allow to validate
model on demand. Also note that validations will run when using that methods.
Attribute change testing
changed?
unchanged?
is_changed?
is_unchanged?
has_changed?
hasnt_changed?
not_changed?
The methods above allow you to test if certain attribute changed recently or not.
Examples:
u = User.first
u.username = ' '
u.the_attribute(:username).has_changed?
# => true
u.the_attribute(:id).unchanged?
# => true
Be aware that this method requires that the used ORM has changes
method that allows to query a model for
all changed attributes.
Attribute virtuality testing
semi_real?
virtual?
is_semi_real?
is_virtual?
The methods above allow to you to test if certain attribute is virtual or semi-real (explained later).
u = User.first
u.the_attribute(:id).is.virtual?
# => false
u.the_attribute(:id).is.semi_real?
# => false
Attribute value querying
value
Using the value
method you can get the current value of an attribute.
Example:
u = User.first
u.the_attribute.username.value
# => "admin"
Attribute filters
Having attribute sets defined we can make use of them in many different ways. One is filtering attributes, either by hand or with a little help of ORM callbacks. The Attribute Filters library brings some DSL methods to make that task easy.
First of all, it won't create any callbacks for you nor register them in your models. You have to create filtering methods manually and call them (or add their names) in the right places. That way you still have control over the filtering process and its order. The thing that is different with Attribute Filters is that you can create filtering methods without reffering to particular attributes but to sets defined earlier.
Example:
class User < ActiveRecord::Base
# Defining attribute sets
its_attribute real_name: [ :should_be_stripped, :should_be_capitalized ]
its_attribute username: [ :should_be_stripped, :should_be_downcased ]
its_attribute email: [ :should_be_stripped, :should_be_downcased ]
# Registering filtering callback methods
before_validation :strip_names
before_validation :downcase_names
before_validation :capitalize_names
# Defining filtering methods
has_filtering_method :downcase_names, :should_be_downcased
has_filtering_method :capitalize_names, :should_be_capitalized
has_filtering_method :strip_names, :should_be_stripped
def downcase_names
filter_attributes_that :should_be_downcased do |atr|
atr.mb_chars.downcase.to_s
end
end
def capitalize_names
filter_attributes_that :should_be_capitalized do |atr|
atr.mb_chars.split(' ').map { |n| n.capitalize }.join(' ')
end
end
def strip_names
for_attributes_that(:should_be_stripped) { |atr| atr.strip! }
end
end
What we see here are filtering clauses that are responsible
for altering attribute values: for_attributes_that
and
filter_attributes_that
.
Filtering attributes
Filtering attributes basically means calling a proper method that will collect the attributes (matching certain criteria and belonging to the given set) and calling some code block that will either produce new values or just call some method on each of current attribute value and name.
filter_attrs_from_set(set_name,...)
The filter_attrs_from_set
(a.k.a filter_attributes_that
) method is used for altering values of all attributes that belong to the given set.
It takes optional arguments and a block. The result of evaluating a block will become a new value for a processed
attribute. Optional arguments will be passed as the last arguments of a block.
The evaluated block can make use of the following arguments that are passed to it:
attribute_value
[Object] - current attribute value that should be alteredattribute_name
[String] - a name of currently processed attributeset_object
[Object] - currently processed set that attribute belongs toset_name
[Symbol] - a name of the processed attribute setargs
[Array] - an optional arguments passed to the method
By default only existing, changed and non-blank attributes are processed. You can change that behavior by adding a flags as the first arguments:
:process_blank
– tells to also process attributes that are blank (empty ornil
):process_all
- tells to process all attributes, not just the ones that has changed:no_presence_check
– tells not to check for existence of each processed attribute when processing all attributes; increases performance but you must care about putting only the existing attributes into sets
The checking mentioned in :no_presence_check
flag description is done by querying internal Rails hashes
containing known attributes and internal sets containing virtual attributes added by attr_virtual
or
treats_as_real
keywords (described later).
Example:
class User
has_attribute_that should_be_lolized: :username
before_validation :lolize
def lolize
filter_attributes_that(:should_be_lolized, "lol") do |attribute_value, attribute_name, set_object, set_name, *args|
[attribute_value, set_name.to_s, attribute_name, *args.flatten].join('-')
end
end
filtering_method :lolize, :should_be_lolized
end
u = User.new
u.username = 'john'
u.valid?
u.username
# => "john-should_be_lolized-username-lol"
You can pass a set object (AttributeSet
instance) instead of set name as an argument. The method
will then work on that local data with one difference: the set_name
passed to a block will be set to nil
.
If the block is not given then the method returns an enumerator which can be used to query method later (when the block is present).
Instead of filter_attrs_from_set
you may also use one of the aliases:
attribute_filter_for_set
,filter_attributes_which
,
filter_attributes_that
,filter_attributes_that_are
,filter_attributes_which_are
,
alter_attributes_which
,alter_attributes_that
,alter_attributes_that_are
,alter_attributes_which_are
for_each_attr_from_set(set_name,...)
The for_each_attr_from_set
(a.k.a for_attributes_that
) method is used for iterating over all attributes that belong to the given set.
It takes optional arguments and a block. The result of evaluating the given block is ignored
but you can interact with the attribute object from within a block.
Optional arguments will be passed as the last arguments of a block.
The evaluated block can make use of the following arguments that are passed to it:
attribute_value
[Object] - current attribute value that should be alteredattribute_name
[String] - a name of currently processed attributeset_object
[Object] - currently processed set that attribute belongs toset_name
[Symbol] - a name of the processed attribute setargs
[Array] - an optional arguments passed to the method
By default only existing, changed and non-blank attributes are processed. You can change that behavior by adding a flags as the first arguments:
:process_blank
– tells to also process attributes that are blank (empty ornil
):process_all
- tells to process all attributes, not just the ones that has changed:no_presence_check
– tells not to check for existence of each processed attribute when processing all attributes; increases performance but you must care about putting only the existing attributes into sets:include_missing
– includes attributes that does not exist in a resulting iteration (their values are alwaysnil
); has the effect only when:process_blank
and:no_presence_check
are present
The checking mentioned in :no_presence_check
flag description is done by querying internal Rails hashes
containing known attributes and internal sets containing virtual attributes added by attr_virtual
or
treats_as_real
keywords (described later).
Example:
class User
has_attribute_that should_be_lolized: :username
before_validation :lolize
def lolize
for_attributes_that(:should_be_lolized, :process_blank, "lol") do |attribute_value, attribute_name, set_object, set_name, *args|
attribute_object << '-' << [set_name.to_s, attribute_name, *args.flatten].join('-')
end
end
filtering_method :lolize, :should_be_lolized
end
u = User.new
u.username = 'john'
u.valid?
u.username
# => "john-should_be_lolized-username-lol"
You can pass a set object (AttributeSet
instance) instead of set name as an argument. The method
will then work on that local data with one difference: the set_name
passed to a block will be set to nil
.
If the block is not given then the method returns an enumerator which can be used to query method later (when the block is present).
Instead of filter_attrs_from_set
you may also use one of the aliases:
attribute_call_for_set
,call_attrs_from_set
,
for_attributes_which
,for_attributes_that
,for_attributes_that_are
,for_attributes_which_are
attributes_to_filter(set,...)
The attributes_to_filter
method is used for getting the attributes that should be filtered.
This is a core method used by other filtering methods to establish a collection of attributes
to be processed but you can use it on your own. It returns an AttributeSet
instance containing
attribute names that match the given criteria (by default: that are in a set of the given name,
that are present and that have changed lately).
The method takes one manatory argument (set
) and two optional arguments (process_all
and no_presence_check
)
that can be true
or false
(default). The set
may be an object which is kind of String, Symbol (in that case it should contain the name of a set) or it can be an object which is a kind of AttributeSet
(in that case it should contain a proper object). The first optional argument (process_all
), when set to true
, forces method to return also the attributes that haven't changed lately.
By default the result will be narrowed to the attributes that have changed and haven't been saved yet.
The second optional argument (no_presence_check
) will tell the method to omit the presence check
for each attribute. By default only the attributes that are real attributes (are present
in attributes
hash) are collected. This mehtod does not check the value of attributes.
Example:
u = User.first
u.email = "[email protected]"
u.attributes_to_filter(:should_be_stripped)
# => #<ActiveModel::AttributeSet: {"email"}>
In the example above only the email attribute was listed since it would be filtered if the filtering operation occured.
Filtering virtual attributes
There are cases that virtual attributes (the ones that does not really exist in a database) are to be filtered. By default it won't happen since all the filtering methods assume the presence of any processed attribute as 'a real' attribute. There are different ways to overcome that problem. First two methods described below are recommended.
Marking as virtual
Since version 1.4.0 of Attribute Filters the recommended way of dealing with
virtual attributes is to make use of Active Model's changes tracking.
To do that look at your ORM's documentation and see
ActiveModel::Dirty
and a method attribute_will_change
that it contains. Just call that method from within
your setter and you're done.
This approach can be easily used with predefined filters. The benefit of it, contrary to other methods, is that a filter will never be called redundantly.
You should use the built-in DSL keyword attr_virtual
that will enable changes
tracking for your attribute.
class User < ActiveRecord::Base
# declare a virtual attribute
attr_virtual :real_name
# define a set
has_attributes_that :should_be_splitted => [ :real_name ]
# register a callback method
before_validation :split_attributes
# create a filtering method
def split_attributes
for_attributes_that(:should_be_splitted) do |value|
names = value.split(' ')
self.first_name = names[0]
self.last_name = names[1]
end
end
filtering_method :split_attributes, :should_be_splitted
end
Note that for Rails version 3 you may need to declare attribute as accessible using attr_accessible
if you want controllers to be able to update its value through assignment passed to model.
You have to create setter and getter for virtual attributes on you own.
It may be done before or after attr_virtual
keyword.
Example:
class User < ActiveRecord::Base
# define a set
has_attributes_that :should_be_splitted => [ :real_name ]
# register a callback method
before_validation :split_attributes # you could also use :filter_attributes here
# create a filtering method
def split_attributes
for_attributes_that(:should_be_splitted) do |value|
names = value.split(' ')
self.first_name = names[0]
self.last_name = names[1]
end
end
filtering_method :split_attributes, :should_be_splitted
# inform AF that the model has a virtual attribute
has_virtual_attribute :real_name
# own setter
def real_name=(val)
# do somehing specific here (or not)
@real_name = val
end
# own getter
def real_name
# do somehing specific here (or not)
@real_name
end
end
Instead of attr_virtual
you may also use its alias:
has_virtual_attribute
There are (rare) cases when you may wish to treat attributes as virtual but without automagically wrapping their setter
methods to track changes (e.g. setter is already doing it manually). In such cases make sure that the setter and getter exist and use low-level method called treat_as_virtual
, for instance:
class User < ActiveRecord::Base
splits_attributes :real_name
before_validation :filter_attributes
# inform AF that the model has a virtual attribute
# and changes tracking is already implemented
treats_as_virtual :real_name
# own getter
attr_reader :real_name
# own setter that implements changes tracking
def real_name=(val)
attribute_will_change!('real_name') if val != real_name
@real_name = val
end
end
Unconditional filtering
The next way to filter virtual attribute is to pass the :no_presence_check
and :process_all
flags
to the filtering method. That causes filter to be executed for each attribute from a set without
any conditions (no checking for presence of setter/getter method and no checking for changes).
Be aware that by doing that you take full responsibility for attribute set names added to your set. If you put a name of nonexistent attribute then you may get ugly error later.
With that approach you can filter virtual attributes that are inaccessible to controllers
and don't show up when model is queried for known attributes. Using it may also cause some filters
to be executed more often than needed, since the changes tracking is disabled (process_all
).
Example:
class User
# define a set
has_attributes_that :should_be_splitted => [ :real_name ]
# register a callback method
before_validation :split_attributes # you could also use :filter_attributes here
# create a filtering method
def split_attributes
for_attributes_that(:should_be_splitted, :no_presence_check, :process_all) do |value|
names = value.split(' ')
self.first_name = names[0] # assuming first_name exists in a database
self.last_name = names[1] # assuming last_name exists in a database
end
end
filtering_method :split_attributes, :should_be_splitted
end
Marking as semi-real
There is also a way that bases on using treats_attributes_as_real
(or simply treats_as_real
) clause in your model
(available from version 1.2.0 of Attribute Filters). That approach may be applied to attributes
that aren't (may not or should not be) tracked for changes and aren't (may not or should not be) accessible
(to a controller) nor marked as protected (if using Rails version <= 3).
There are two main differences from the unconditional filtering. First is that marking attribute
as real causes it to be added to the list of all known attributes that is returned by all_attributes_set
(a.k.a all_attributes
). The second is that nothing ugly will happen if there will be non-existent
attributes in a set when filtering method kicks in. That's because the filtering method doesn't require
:no_presence_check
flag to pick up such attributes. Attributes that cannot be handled are simply ignored.
The :process_all
flag is also not needed since all attributes marked as semi-real are by default
not checked for changes.
Example:
class User
# mark the attribute as real
treats_as_real :real_name
# define a set
has_attributes_that :should_be_splitted => [ :real_name ]
# register a callback method
before_validation :split_attributes # you could also use :filter_attributes here
# create a filtering method
def split_attributes
for_attributes_that(:should_be_splitted) do |value|
names = value.split(' ')
self.first_name = names[0] # assuming first_name exists in a database
self.last_name = names[1] # assuming last_name exists in a database
end
end
filtering_method :split_attributes, :should_be_splitted
end
Instead of treat_as_real
you may also use one of its aliases:
attribute_filters_semi_real
,treat_attribute_as_real
,treat_attributes_as_real
,treats_attribute_as_real
,treats_attributes_as_real
,treats_as_real
,
Annotations
Annotations are portions of data that you can bind to attribute names residing within attribute sets. What for? To store something that is related to the specific attribute and that should be memorized within a set and/or its copies (if any). You can annotate each attribute name using key -> value pairs where the key is always a symbol and value is any kind of object you want. Only existing attributes can be annotated and deleting attribute will remove annotations that are assigned to it.
When you copy a set, create difference or intersection of attribute sets, any existing annotations are also copied. If the operation creates a sum or joins sets then the annotations are mixed too.
The annotations are used by some of the predefined filters described later to precise operations that have to be taken (e.g. specifying separator string for attribute joining filter and so on).
Creating annotations
You can create annotations during defining a set or you can add them later with annotate_attribute_set
.
In case of local sets you can also use a method called annotate
.
When defining sets
When using the first method just replace attrbute name with a hash, where attribute name is a key and annotations are another hash containing keys and values.
Example:
class User
# Set name: cool
# Attribute name: email
# Annotation key: some_key
# Annotation value: some value
has_attributes_that_are cool: { :email => { :some_key => "some value" } }
end
The above will annotate email
attribute within a set should_be_something
with some_key
=> "some value" pair.
You can mix annotated attributes with unannotated; just put the last ones in front of an array:
class User
has_attributes_that_are cool: [ :some_unannotated, { :email => { :some_key => "some value" } } ]
end
or
class User
has_attributes_that_are :cool => [ :some_unannotated, { :email => { :some_key => "some value" } } ]
end
After defining sets
To create annotations for class-level sets use the annotate_attribute_set
method or one of its aliases:
annotate_attributes_that_are
annotate_attributes_that
annotate_attributes_are
annotate_attributes_for
annotate_attributes_set
annotate_properties_that
annotate_attributes
annotates_attributes_that_are
annotates_attributes_that
annotates_attributes_are
annotates_attributes_for
annotates_attributes_set
annotates_properties_that
annotates_attributes
attribute_set_annotate
Example:
class User
has_attributes_that_are cool: [ :some_unannotated, :email, :username ]
annotates_attributes_that_are cool: [ :email, :some_key, "some value" ]
annotates_attributes_that_are :cool => { :username => { :some_key => "some value", :other_k => 'other v' } }
end
Caution: Annotating attributes that aren't present in a set
with annotate_attribute_set
will raise an error.
Annotating local sets
annotate
Example:
class User
def some_method
s = ActiveModel::AttributeSet.new([:first_element, :second_element])
s.annotate(:first_element, :key, 'value')
end
end
Removing annotations
To remove annotations locally (from sets that are not directly bound to models) you can use:
delete_annotation(attribute_name, annotation_key)
- to delete specified annotation key for the given attributedelete_annotations(attribute_name)
- to delete all annotations for an attribute of the given nameremove_annotations()
- to remove all annotations from a set
Be aware that using these method to delete annotations from class-level sets won't work. That's because you'll always get a copy when querying these sets. However there are methods that will work in a model:
delete_annotation_from_set(set_name, attribute, *annotation_keys)
- to delete annotation keys for the given attributedelete_annotation_from_set(set_name, attribute)
- to delete all annotation keys for the given attributedelete_annotation_from_set(set_name => *attributes)
- to delete all annotation keys for the given attributedelete_annotation_from_set(set_name => { attribute => keys})
- to delete specified annotation keys for the given attributes
Instead of delete_annotation_from_set
you may use the aliases:
delete_annotations_from_set
,delete_annotation_from_set
,deletes_annotations_from_set
Example:
class User
has_attributes_that_are cool: [ :some_unannotated, :email ]
# That will work
deletes_annotations_from_set cool: :email
deletes_annotations_from_set cool: [ :email, :key_one ]
deletes_annotation_from_set cool: { :email => [:key_one, :other_key], :name => :some_key }
# That won't affect the global set called 'cool'
# since we have its copy, not the original.
def some_method
attributes_that_are(:cool).delete_annotation(:email)
end
# That won't affect the global set called 'cool'
# since the method returns its copy, not the original.
attributes_that_are(:cool).delete_annotation(:email)
end
Updating annotations
Calling annotate
method again on a set or redefining set at the class-level allows to add annotations
or to modify their keys.
Example:
class User
has_attributes_that_are :cool => { :email => { :some_key => "some value" } }
has_attributes_that_are cool: { :email => { :other_key => "other_value" } }
has_attributes_that_are cool: { :email => { :some_key => "another_value" } }
annotates_attributes_that_are :cool, :email, :some_key => "x"
deletes_annotation_from_set :cool => { :email => :other_key }
end
# In the result there will be only one annotation key left;
# :some_key with the value of "x"
Caution: Annotating attributes that aren't present in a set
with annotate_attribute_set
or by using annotate
method will raise an error.
Be aware that updating annotations directly (using annotate
method) won't work on sets
defined directly in classes describing models. That's because you'll always get
a copy when querying these sets.
class User
has_attributes_that_are :cool => { :email => { :some_key => "some value" } }
# Calling `some_method` won't work on 'cool' global set.
def some_method
attributes_that_are(:cool).delete_annotation(:email, :other_key)
end
end
Querying annotations
To check if a set has any annotations you can use one of the methods:
has_annotation?
- checks if a set has any annotationshas_annotations?
- checks if a set has any annotationshas_annotation?(attribute_name)
- checks if a set has any annotations for the given attributehas_annotation?(attribute_name, *annotation_keys)
- checks if a set has any annotation key for the given attribute
To read annotations you can use :
annotation(attribute_name)
- gets a hash of annotations or returnsnil
annotation(attribute_name, *keys)
- gets an array annotation values for the given keys (putsnil
s if key is missing) or returnsnil
Example:
class User
attributes_that_are cool: [ :some_unannotated, :email => { :x => :y } ]
def q
attributes_that_are(:cool).annotation(:email, :x, :z)
end
def qq
attributes_that_are(:cool).annotation(:nope, :x, :z)
end
# Calling q will return an array: [:y, nil]
# Calling qq will return nil since attribute is not present (or not annotated)
end
To read all annotations use annotations
method called on an attribute set. For instance:
User.first.attributes_that(:should_be_splitted).annotations
# => {"some_name"=>{:split_into=>[:first_part, :second_part]}}
As you can see it returns a hash with attribute names as keys and annotations as values. If some attribute from a set doesn't have any annotations then it's not included in the results.
Predefined filters
Predefined filters are ready-to-use methods for filtering attributes. You just have to call them or register them as callbacks.
To use all predefined filters you have to manually
include the ActiveModel::AttributeFilters::Common
module. That will include all available filtering methods into your model.
Predefined filters work by utilizing global attribute sets with predetermined names. For example,
a common filter that downcases attributes will use the set called :should_be_downcased
.
Example:
class User < ActiveRecord::Base
include ActiveModel::AttributeFilters::Common
its_attribute name: [:should_be_downcased, :should_be_titleized ]
before_validation :downcase_attributes
before_validation :titleize_attributes
end
If you don't want to include portions of code that you'll never use in your model classes, you may want to include some filters selectively. To do that just include a submodule containing needed filtering method:
class User < ActiveRecord::Base
include ActiveModel::AttributeFilters::Common::Downcase
include ActiveModel::AttributeFilters::Common::Titleize
its_attribute name: [:should_be_downcased, :should_be_titleized ]
before_validation :downcase_attributes
before_validation :titleize_attributes
end
As you can see, to use any filter you should include a proper submodule, add attribute
names to a proper set and register a callback. The name of a set
that a filtering method will use is predetermined and follows the
convention that it should correspond to the name of a filter. So the
set used by squeezing filter will be named should_be_squeezed
.
For example, to squeeze attributes name
and email
you can write:
class User < ActiveRecord::Base
include ActiveModel::AttributeFilters::Common::Squeeze
has_attributes_that should_be_squeezed: [:email, :name]
before_validation :squeeze_attributes
end
Filtering keywords
The filtering methods usually come with class-level DSL methods
that are helpful wrappers. They are simply calling the_attribute
method to add some attributes to a proper set for you.
So you can also write:
class User < ActiveRecord::Base
include ActiveModel::AttributeFilters::Common::Squeeze
squeezes_attributes :email, :name
before_validation :squeeze_attributes
end
Some of these helpers are doing some extra work to make things syntactically easier than with using pure sets. Check the list if common filters for more information.
Calling all filters
There is a special method called
filter_attributes
that can be registered as a callback. It will call all filtering methods which sets (that they're assigned to)
are non-empty. In other words it will run specific filtering method for each global attribute set if the set exists
and contains at least one element. The calling order is the same as the order of adding sets in your model class.
Example:
class User < ActiveRecord::Base
include ActiveModel::AttributeFilters::Common::Squeeze
include ActiveModel::AttributeFilters::Common::Capitalize
squeezes_attributes :email, :name
capitalizes_attribute :name
before_validation :filter_attributes
end
Or even more simpler (but it will include all common filters):
class User < ActiveRecord::Base
include ActiveModel::AttributeFilters::Common
squeezes_attributes :email, :name
capitalizes_attribute :name
before_validation :filter_attributes
end
The method of using common filters presented in he example above is the easiest and the most consistent one.
Of course the filter_attributes
will also work if the sets are
specified explicitly:
class User < ActiveRecord::Base
include ActiveModel::AttributeFilters::Common
has_attributes_that :should_be_squeezed => [ :email, :name ]
has_attributes_that :should_be_capitalized => :name
before_validation :filter_attributes
end
To know which methods are known to filter_attributes
use the filtering_methods
instance method
available in your model. It returns a meta set containing
attributes set names as keys and the methods assigned
to them as values (both are symbols).
Note that the method above returns all known methods marked as filtering methods, including those that might not be called until a proper set will be defined. To check what methods will actually be called (at a given time) use:
(attribute_sets & filtering_methods).values
Custom filtering order
Instead of relaying on filter_attributes
you may create
your own callback method containing invocations of your
filtering methods in the desired order (independent from
the "natural" order, based on adding the sets to model class):
class User < ActiveRecord::Base
include ActiveModel::AttributeFilters::Common
has_attributes_that :should_be_squeezed => [ :email, :name ]
has_attribute_that :should_be_capitalized => :name
before_validation :prepare_attributes
def prepare_attributes
capitalize_attributes # common filter that uses the set called :should_be_capitalized
squeeze_attributes # common filter that uses the set called :should_be_squeezed
end
end
Or you can just register multiple callbacks to achieve the same result.
Custom filtering methods
You can create your own filtering methods and they will be called
among others by filter_attributes
. You just have to use special
DSL class method called filtering_method
to mark your method
and assign its name to some global attribute set (not used by
any other filtering method).
Synopsis:
filtering_method(method_name, set_name)
The global set of the given name will be looked up when callback
method filter_attributes
will be called and if non-empty then
the registered method will be called (among others).
Example:
class User < ActiveRecord::Base
include ActiveModel::AttributeFilters::Common
has_attributes_that should_be_happy: [ :username, :real_name ]
before_validation :filter_attributes
def happify
filter_attributes_that(:should_be_happy) do |v, o|
v + "-happy"
end
end
filtering_method :happify, :should_be_happy
end
Of course, you can set your own method as a callback explicitly
instead of using the filter_attributes
.
Data types
The common filters are aware of different kinds of data and can operate on attributes that are arrays or hashes. If an array or a hash is detected then the filtering is made recursively for each element (or for each value in case of a hash) and the produced value is returned. If the attribute has an unknown type then its value is not altered at all and left intact.
Some of the common filters may treat arrays and hashes in a slight different way (e.g. joining and splitting filters do that).
The common filters are aware of multibyte strings so string operations should handle diacritics properly.
List of filters
See the COMMON-FILTERS for detailed descriptions and usage examples.
See the
ActiveModel::AttributeFilters::Common
for descriptions of common filtering modules.
Custom applications
You may want to use attribute sets for custom logic, not just for the filtering purposes. For example, you may have some set of attributes that all have to exist for a user to make deals. Then you may have some method that checks if user is able to trade by testing the presence of attributes from the defined set.
Example:
class User < ActiveRecord::Base
has_attributes_that_are required_to_trade: [ :username, :email, :real_name, :address, :account ]
def is_able_to_trade?
are_attributes_that_are.required_to_trade.all.present? and
are_attributes_that_are.required_to_trade.all.valid?
end
def attributes_missing_to_trade
attributes_that_are.required_to_trade.list.blank? +
attributes_that_are.required_to_trade.list.invalid?
end
end