Class: Delineate::AttributeMap
- Includes:
- Schema, Serialization
- Defined in:
- lib/delineate/attribute_map.rb
Overview
Attribute Maps
The AttributeMap class provides the ability to expose an ActiveRecord model’s attributes and associations in a customized way. By specifying an attribute map, the model’s internal attributes and associations can be de-coupled from its presentation or interface, allowing a consumer’s interaction with the model to remain consistent even if the model implementation or schema changes.
Note: Although this description contemplates usage of an attribute map in terms of defining an API, multiple attribute maps can be constructed exposing different model interfaces for various use cases.
To define an attribute map in an ActiveRecord model, do the following:
class Account < ActiveRecord::Base
map_attributes :api do
attribute :name
attribute :path, :access => :ro
attribute :active, :access => :rw, :using => :active_flag
end
end
The map_attributes class method establishes an attribute map that will be used by the model’s <map-name>_attributes and <map-name>_attributes= methods. This map specifies the attribute names, access permissions, and other options as viewed by a user of the model’s public API. In the example above, 3 of the the model’s attributes are exposed through the API.
Mapping Model Attributes
To declare a model attribute be included in the map, you use the attribute method on the AttributeMap instance:
attribute :public_name, :access => :rw, :using => :internal_name
The first parameter is required and is the map-specific public name for the attribute. If the :using parameter is not provided, the external name and internal name are assumed to be identical. If :using is specified, the name provided must be either an existing model attribute, or a method that will be called when reading/writing the attribute. In the example above, if internal_name is not a model attribute, you must define methods internal_name() and internal_name=(value) in the ActiveRecord class, the latter being required if the attribute is not read-only.
The :access parameter can take the following values:
:rw This value, which is the default, means that the attribute is read-write. :ro The :ro value designates the attribute as read-only. Attempts to set the
attribute's value will be ignored.
:w The attribute value can be set, but does not appear when the attributes
read.
:none Use this option when merging in a map to ignore the attribute defined in
the other map.
The :optional parameter affects the reading of a model attribute:
attribute :balance, :access => :ro, :optional => true
Optional attributes are not accessed/included when retrieving the mapped attributes, unless explicitly requested. This can be useful when there are performance implications for getting an attribute’s value, for example. You can specify a symbol as the value for :optional instead of true. The symbol then groups together all attributes with that option group. For example, if you specify:
attribute :balance, :access => :ro, :optional => :compute_balances
attribute :total_balance, :access => :ro, :optional => :compute_balances
you then get:
acct.api_attributes(:include => :balance) # :balance attribute is included in result
acct.api_attributes(:include => :compute_balances) # Both :balance and :total_balance attributes are returned
The :read and :write parameters are used to define accessor methods for the attribute. The specified lambda will be defined as a method named by the :model_attr parameter. For example:
attribute :parent,
:read => lambda {|a| a.parent ? a.parent.path : nil},
:write => lambda {|a, v| a.parent = {:path => v}}
Two methods, parent_<map-name>() and parent_<map_name>=(value) will be defined on the ActiveRecord model.
Mapping Model Associations
In addition to model attributes, you can specify a model’s associations in an attribute map. For example:
class Account < ActiveRecord::Base
:belongs_to :account_type
map_attributes :api do
attribute :name
attribute :path, :access => :ro
association :type, :using => :account_type
end
end
The first parameter in the association specification is its mapped name, and the optional :using parameter is the internal association name. In the the example above the account_type association is exposed as a nested object named ‘type’.
When specifying an association mapping, by default the attribute map in the association’s model class is used to define its attributes and nested associations. If you include an attribute defininiton in the association map, it will override the spec in the association model:
class Account < ActiveRecord::Base
:belongs_to :account_type
map_attributes :api do
association :type, :using => :account_type do
attribute :name, :access => :ro
attribute :description, :access => :ro
end
end
end
In this example, if the AccountType attribute map declared :name as read-write, the association map in the Account model overrides that to make :name read-only when accessed as a nested object from an Account model. If the :description attribute of AccountType had not been specified in the AccountType attribute map, the inclusion of it here lets that attribute be exposed in the Account attribute map. Note that when overriding an association’s attribute, the override must completely re-define the attribute’s options.
If you want to fully specify an association’s attributes, use the :override option as follows:
class Account < ActiveRecord::Base
:belongs_to :account_type
map_api_attributes :account do
association :type, :using => :account_type, :override => :replace do
attribute :name, :access => :ro
attribute :description, :access => :ro
association :category, :access => :ro :using => :account_category
attribute :name
end
end
end
end
which re-defines the mapped association as viewed by Account; no merging is done with the attribute map defined in the AccountType model. In the example above, note the ability to nest associations. For this to work, account_category must be declared as an ActiveRecord association in the AccountType class.
Other parameters for mapping associations:
:access As with attributes, an association can be declared :ro or :rw (the
default). An association that is writeable will be automatically
specified in an accepts_nested_attributes_for, which allows
attribute writes to contain a nested hash for the association
(except for individual association attributes that are read-only).
:optional When set to true, the association is not included by default when
retrieving/returning the model's mapped attributes.
:polymorphic Affects reading only and is relevant when the association class
is an STI base class. When set to true, the attribute map of
each association record (as defined by its class) is used to
specify its included attributes and associations. This means that
in a collection association, the returned attribute hashes may be
heterogeneous, i.e. vary according to each retrieved record's
class. NOTE: when using :polymorphic, you cannot merge/override
the association class attribute map.
STI Attribute Maps
ActiveRecord STI subclasses inherit the attribute maps from their superclass. If you want to include additional subclass attributes, just invoke map_attributes in the subclass and define the extra attributes and associations. If the subclass wants to completely override/replace the superclass map, do:
class MySubclass < MyBase
map_attributes :api, :override => :replace do
.
.
end
end
Instance Attribute Summary collapse
-
#associations ⇒ Object
Returns the value of attribute associations.
-
#attributes ⇒ Object
Returns the value of attribute attributes.
-
#klass_name ⇒ Object
readonly
Returns the value of attribute klass_name.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
Instance Method Summary collapse
-
#association(name, options = {}, &blk) ⇒ Object
Declare an association to be included in the map.
-
#attribute(*args) ⇒ Object
Declare a single attribute to be included in the map.
- #copy(other_map) ⇒ Object
-
#dup ⇒ Object
Returns a new copy of this AttributeMap instance.
-
#initialize(klass_name, name, options = {}) ⇒ AttributeMap
constructor
The klass constructor parameter is the ActiveRecord model class for the map being created.
-
#merge!(other_attr_map, merge_opts = {}) ⇒ Object
Merges another AttributeMap instance into this instance.
-
#resolve(must_resolve = false, resolving = []) ⇒ Object
Attempts to resolve the map and the maps it depends on.
-
#resolve! ⇒ Object
Will raise an exception of the map cannot be fully resolved.
- #resolved? ⇒ Boolean
Methods included from Schema
Methods included from Serialization
#association_attribute_map, #attribute_value, #map_attributes_for_write, #model_association, #serializable_association_names, #serializable_attribute_names
Constructor Details
#initialize(klass_name, name, options = {}) ⇒ AttributeMap
The klass constructor parameter is the ActiveRecord model class for the map being created.
198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/delineate/attribute_map.rb', line 198 def initialize(klass_name, name, = {}) @klass_name = klass_name @name = name @options = () @attributes = {} @associations = {} @write_attributes = {:_destroy => :_destroy} @resolved = false @sti_baseclass_merged = false end |
Instance Attribute Details
#associations ⇒ Object
Returns the value of attribute associations.
194 195 196 |
# File 'lib/delineate/attribute_map.rb', line 194 def associations @associations end |
#attributes ⇒ Object
Returns the value of attribute attributes.
193 194 195 |
# File 'lib/delineate/attribute_map.rb', line 193 def attributes @attributes end |
#klass_name ⇒ Object (readonly)
Returns the value of attribute klass_name.
191 192 193 |
# File 'lib/delineate/attribute_map.rb', line 191 def klass_name @klass_name end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
192 193 194 |
# File 'lib/delineate/attribute_map.rb', line 192 def name @name end |
Instance Method Details
#association(name, options = {}, &blk) ⇒ Object
Declare an association to be included in the map.
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/delineate/attribute_map.rb', line 238 def association(name, = {}, &blk) (, block_given?) model_attr = ([:model_attr] || name).to_sym reflection = model_association_reflection(model_attr) attr_map = .delete(:attr_map) || AttributeMap.new(reflection.class_name, @name) attr_map.instance_variable_set(:@options, {:override => [:override]}) if [:override] attr_map.instance_eval(&blk) if block_given? if !merge_option?() && attr_map.empty? raise ArgumentError, "Map association '#{name}' in class #{@klass_name} specifies :replace but has empty block" end if [:access] != :ro and !klass.accessible_attributes.include?(model_attr.to_s+'_attributes') raise "Expected attr_accessible and/or accepts_nested_attributes_for :#{model_attr} in #{@klass_name} model" end @associations[name] = {:klass_name => reflection.class_name, :options => , :attr_map => attr_map.empty? ? nil : attr_map, :collection => (reflection.macro == :has_many || reflection.macro == :has_and_belongs_to_many)} end |
#attribute(*args) ⇒ Object
Declare a single attribute to be included in the map. You can declare a list, but the attribute options are limited to :access and :optional.
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/delineate/attribute_map.rb', line 213 def attribute(*args) = args. (, args.size) args.each do |name| if [:access] == :none @attributes.delete(name) @write_attributes.delete(name) else @attributes[name] = model_attr = ([:model_attr] || name).to_sym model_attr = define_attr_methods(name, model_attr, ) unless is_model_attr?(model_attr) if [:access] != :ro if model_attr.to_s != klass.primary_key && !klass.accessible_attributes.detect { |a| a == model_attr.to_s } raise "Expected 'attr_accessible :#{model_attr}' in #{@klass_name}" end @write_attributes[name] = model_attr end end end end |
#copy(other_map) ⇒ Object
289 290 291 292 293 294 295 296 297 298 |
# File 'lib/delineate/attribute_map.rb', line 289 def copy(other_map) @attributes = other_map.attributes.deep_dup @write_attributes = other_map.instance_variable_get(:@write_attributes).deep_dup @associations = other_map.associations.deep_dup @resolved = other_map.instance_variable_get(:@resolved) @sti_baseclass_merged = other_map.instance_variable_get(:@sti_baseclass_merged) self end |
#dup ⇒ Object
Returns a new copy of this AttributeMap instance
278 279 280 281 282 283 284 285 286 287 |
# File 'lib/delineate/attribute_map.rb', line 278 def dup returning self.class.new(@klass_name, @name) do |map| map.attributes = @attributes.dup map.instance_variable_set(:@write_attributes, @write_attributes.dup) map.associations = @associations.dup map.instance_variable_set(:@resolved, @resolved) map.instance_variable_set(:@sti_baseclass_merged, @sti_baseclass_merged) end end |
#merge!(other_attr_map, merge_opts = {}) ⇒ Object
Merges another AttributeMap instance into this instance.
262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/delineate/attribute_map.rb', line 262 def merge!(other_attr_map, merge_opts = {}) return if other_attr_map.nil? @attributes = @attributes.deep_merge(other_attr_map.attributes) @associations.deep_merge!(other_attr_map.associations) @write_attributes = {:_destroy => :_destroy} @attributes.each {|k, v| @write_attributes[k] = (v[:model_attr] || k) unless v[:access] == :ro} @options = other_attr_map.instance_variable_get(:@options).dup if merge_opts[:with_options] @resolved = other_attr_map.resolved? if merge_opts[:with_state] self end |
#resolve(must_resolve = false, resolving = []) ⇒ Object
Attempts to resolve the map and the maps it depends on. If must_resolve is truthy, will raise an exception if map cannot be resolved.
312 313 314 315 316 317 318 319 320 321 322 323 |
# File 'lib/delineate/attribute_map.rb', line 312 def resolve(must_resolve = false, resolving = []) return true if @resolved return true if resolving.include?(@klass_name) # prevent infinite recursion resolving.push(@klass_name) result = resolve_associations(must_resolve, resolving) result = false unless resolve_sti_baseclass(must_resolve, resolving) resolving.pop @resolved = result end |
#resolve! ⇒ Object
Will raise an exception of the map cannot be fully resolved
305 306 307 308 |
# File 'lib/delineate/attribute_map.rb', line 305 def resolve! resolve(:must_resolve) self end |
#resolved? ⇒ Boolean
300 301 302 |
# File 'lib/delineate/attribute_map.rb', line 300 def resolved? @resolved end |