delineate 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +90 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +292 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/delineate.gemspec +84 -0
- data/lib/class_inheritable_attributes.rb +141 -0
- data/lib/core_extensions.rb +14 -0
- data/lib/delineate.rb +11 -0
- data/lib/delineate/attribute_map/attribute_map.rb +714 -0
- data/lib/delineate/attribute_map/csv_serializer.rb +133 -0
- data/lib/delineate/attribute_map/json_serializer.rb +37 -0
- data/lib/delineate/attribute_map/map_serializer.rb +170 -0
- data/lib/delineate/attribute_map/xml_serializer.rb +45 -0
- data/lib/delineate/map_attributes.rb +201 -0
- data/spec/database.yml +17 -0
- data/spec/delineate_spec.rb +662 -0
- data/spec/spec_helper.rb +55 -0
- data/spec/support/models.rb +184 -0
- data/spec/support/schema.rb +125 -0
- metadata +182 -0
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.6.0
|
data/delineate.gemspec
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
# stub: delineate 0.6.0 ruby lib
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "delineate"
|
9
|
+
s.version = "0.6.0"
|
10
|
+
|
11
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
s.authors = ["Tom Smith"]
|
14
|
+
s.date = "2014-02-21"
|
15
|
+
s.description = "ActiveRecord serializer DSL for mapping model attributes and associations. Similar to ActiveModel Serializers with many enhancements including bi-directional support, i.e. deserialization."
|
16
|
+
s.email = "tsmith@landfall.com"
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE.txt",
|
19
|
+
"README.rdoc"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".document",
|
23
|
+
".rspec",
|
24
|
+
"Gemfile",
|
25
|
+
"Gemfile.lock",
|
26
|
+
"LICENSE.txt",
|
27
|
+
"README.rdoc",
|
28
|
+
"Rakefile",
|
29
|
+
"VERSION",
|
30
|
+
"delineate.gemspec",
|
31
|
+
"lib/class_inheritable_attributes.rb",
|
32
|
+
"lib/core_extensions.rb",
|
33
|
+
"lib/delineate.rb",
|
34
|
+
"lib/delineate/attribute_map/attribute_map.rb",
|
35
|
+
"lib/delineate/attribute_map/csv_serializer.rb",
|
36
|
+
"lib/delineate/attribute_map/json_serializer.rb",
|
37
|
+
"lib/delineate/attribute_map/map_serializer.rb",
|
38
|
+
"lib/delineate/attribute_map/xml_serializer.rb",
|
39
|
+
"lib/delineate/map_attributes.rb",
|
40
|
+
"spec/database.yml",
|
41
|
+
"spec/delineate_spec.rb",
|
42
|
+
"spec/spec_helper.rb",
|
43
|
+
"spec/support/models.rb",
|
44
|
+
"spec/support/schema.rb"
|
45
|
+
]
|
46
|
+
s.homepage = "http://github.com/rtomsmith/delineate"
|
47
|
+
s.licenses = ["MIT"]
|
48
|
+
s.rubygems_version = "2.2.2"
|
49
|
+
s.summary = "ActiveRecord serializer DSL for mapping model attributes and associations"
|
50
|
+
|
51
|
+
if s.respond_to? :specification_version then
|
52
|
+
s.specification_version = 4
|
53
|
+
|
54
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
55
|
+
s.add_runtime_dependency(%q<activerecord>, ["~> 3.2"])
|
56
|
+
s.add_runtime_dependency(%q<activesupport>, ["~> 3.2"])
|
57
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.14"])
|
58
|
+
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
59
|
+
s.add_development_dependency(%q<bundler>, ["~> 1"])
|
60
|
+
s.add_development_dependency(%q<jeweler>, ["~> 2.0"])
|
61
|
+
s.add_development_dependency(%q<simplecov>, ["~> 0"])
|
62
|
+
s.add_development_dependency(%q<sqlite3>, ["~> 1"])
|
63
|
+
else
|
64
|
+
s.add_dependency(%q<activerecord>, ["~> 3.2"])
|
65
|
+
s.add_dependency(%q<activesupport>, ["~> 3.2"])
|
66
|
+
s.add_dependency(%q<rspec>, ["~> 2.14"])
|
67
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
68
|
+
s.add_dependency(%q<bundler>, ["~> 1"])
|
69
|
+
s.add_dependency(%q<jeweler>, ["~> 2.0"])
|
70
|
+
s.add_dependency(%q<simplecov>, ["~> 0"])
|
71
|
+
s.add_dependency(%q<sqlite3>, ["~> 1"])
|
72
|
+
end
|
73
|
+
else
|
74
|
+
s.add_dependency(%q<activerecord>, ["~> 3.2"])
|
75
|
+
s.add_dependency(%q<activesupport>, ["~> 3.2"])
|
76
|
+
s.add_dependency(%q<rspec>, ["~> 2.14"])
|
77
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
78
|
+
s.add_dependency(%q<bundler>, ["~> 1"])
|
79
|
+
s.add_dependency(%q<jeweler>, ["~> 2.0"])
|
80
|
+
s.add_dependency(%q<simplecov>, ["~> 0"])
|
81
|
+
s.add_dependency(%q<sqlite3>, ["~> 1"])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
|
2
|
+
# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
|
3
|
+
# to, for example, an array without those additions being shared with either their parent, siblings, or
|
4
|
+
# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
|
5
|
+
class Class # :nodoc:
|
6
|
+
def class_inheritable_reader(*syms)
|
7
|
+
syms.each do |sym|
|
8
|
+
next if sym.is_a?(Hash)
|
9
|
+
class_eval <<-EOS
|
10
|
+
def self.#{sym} # def self.before_add_for_comments
|
11
|
+
read_inheritable_attribute(:#{sym}) # read_inheritable_attribute(:before_add_for_comments)
|
12
|
+
end # end
|
13
|
+
#
|
14
|
+
def #{sym} # def before_add_for_comments
|
15
|
+
self.class.#{sym} # self.class.before_add_for_comments
|
16
|
+
end # end
|
17
|
+
EOS
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def class_inheritable_writer(*syms)
|
22
|
+
options = syms.extract_options!
|
23
|
+
syms.each do |sym|
|
24
|
+
class_eval <<-EOS
|
25
|
+
def self.#{sym}=(obj) # def self.color=(obj)
|
26
|
+
write_inheritable_attribute(:#{sym}, obj) # write_inheritable_attribute(:color, obj)
|
27
|
+
end # end
|
28
|
+
#
|
29
|
+
#{" #
|
30
|
+
def #{sym}=(obj) # def color=(obj)
|
31
|
+
self.class.#{sym} = obj # self.class.color = obj
|
32
|
+
end # end
|
33
|
+
" unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
|
34
|
+
EOS
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def class_inheritable_array_writer(*syms)
|
39
|
+
options = syms.extract_options!
|
40
|
+
syms.each do |sym|
|
41
|
+
class_eval <<-EOS
|
42
|
+
def self.#{sym}=(obj) # def self.levels=(obj)
|
43
|
+
write_inheritable_array(:#{sym}, obj) # write_inheritable_array(:levels, obj)
|
44
|
+
end # end
|
45
|
+
#
|
46
|
+
#{" #
|
47
|
+
def #{sym}=(obj) # def levels=(obj)
|
48
|
+
self.class.#{sym} = obj # self.class.levels = obj
|
49
|
+
end # end
|
50
|
+
" unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
|
51
|
+
EOS
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def class_inheritable_hash_writer(*syms)
|
56
|
+
options = syms.extract_options!
|
57
|
+
syms.each do |sym|
|
58
|
+
class_eval <<-EOS
|
59
|
+
def self.#{sym}=(obj) # def self.nicknames=(obj)
|
60
|
+
write_inheritable_hash(:#{sym}, obj) # write_inheritable_hash(:nicknames, obj)
|
61
|
+
end # end
|
62
|
+
#
|
63
|
+
#{" #
|
64
|
+
def #{sym}=(obj) # def nicknames=(obj)
|
65
|
+
self.class.#{sym} = obj # self.class.nicknames = obj
|
66
|
+
end # end
|
67
|
+
" unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
|
68
|
+
EOS
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def class_inheritable_accessor(*syms)
|
73
|
+
class_inheritable_reader(*syms)
|
74
|
+
class_inheritable_writer(*syms)
|
75
|
+
end
|
76
|
+
|
77
|
+
def class_inheritable_array(*syms)
|
78
|
+
class_inheritable_reader(*syms)
|
79
|
+
class_inheritable_array_writer(*syms)
|
80
|
+
end
|
81
|
+
|
82
|
+
def class_inheritable_hash(*syms)
|
83
|
+
class_inheritable_reader(*syms)
|
84
|
+
class_inheritable_hash_writer(*syms)
|
85
|
+
end
|
86
|
+
|
87
|
+
def inheritable_attributes
|
88
|
+
@inheritable_attributes ||= EMPTY_INHERITABLE_ATTRIBUTES
|
89
|
+
end
|
90
|
+
|
91
|
+
def write_inheritable_attribute(key, value)
|
92
|
+
if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
|
93
|
+
@inheritable_attributes = {}
|
94
|
+
end
|
95
|
+
inheritable_attributes[key] = value
|
96
|
+
end
|
97
|
+
|
98
|
+
def write_inheritable_array(key, elements)
|
99
|
+
write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
|
100
|
+
write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
|
101
|
+
end
|
102
|
+
|
103
|
+
def write_inheritable_hash(key, hash)
|
104
|
+
write_inheritable_attribute(key, {}) if read_inheritable_attribute(key).nil?
|
105
|
+
write_inheritable_attribute(key, read_inheritable_attribute(key).merge(hash))
|
106
|
+
end
|
107
|
+
|
108
|
+
def write_inheritable_hiwa(key, hash)
|
109
|
+
write_inheritable_attribute(key, {}.with_indifferent_access) if read_inheritable_attribute(key).nil?
|
110
|
+
write_inheritable_attribute(key, read_inheritable_attribute(key).merge(hash))
|
111
|
+
end
|
112
|
+
|
113
|
+
def read_inheritable_attribute(key)
|
114
|
+
inheritable_attributes[key]
|
115
|
+
end
|
116
|
+
|
117
|
+
def reset_inheritable_attributes
|
118
|
+
@inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
# Prevent this constant from being created multiple times
|
123
|
+
EMPTY_INHERITABLE_ATTRIBUTES = {}.freeze unless const_defined?(:EMPTY_INHERITABLE_ATTRIBUTES)
|
124
|
+
|
125
|
+
def inherited_with_inheritable_attributes(child)
|
126
|
+
inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes)
|
127
|
+
|
128
|
+
if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
|
129
|
+
new_inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
|
130
|
+
else
|
131
|
+
new_inheritable_attributes = inheritable_attributes.inject({}) do |memo, (key, value)|
|
132
|
+
memo.update(key => value.duplicable? ? value.dup : value)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
child.instance_variable_set('@inheritable_attributes', new_inheritable_attributes)
|
137
|
+
end
|
138
|
+
|
139
|
+
alias inherited_without_inheritable_attributes inherited
|
140
|
+
alias inherited inherited_with_inheritable_attributes
|
141
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Object
|
2
|
+
# I like +returning+ in some contexts, despite what ActiveSupport 3+ thinks :)
|
3
|
+
def returning(value)
|
4
|
+
yield(value)
|
5
|
+
value
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Hash
|
10
|
+
# Renames the specified key to the new value
|
11
|
+
def rename_key!(old_key, new_key)
|
12
|
+
self[new_key] = self.delete(old_key)
|
13
|
+
end
|
14
|
+
end
|
data/lib/delineate.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'core_extensions'
|
4
|
+
require 'active_record'
|
5
|
+
|
6
|
+
require 'delineate/map_attributes'
|
7
|
+
require 'delineate/attribute_map/csv_serializer'
|
8
|
+
require 'delineate/attribute_map/xml_serializer'
|
9
|
+
require 'delineate/attribute_map/json_serializer'
|
10
|
+
|
11
|
+
$LOAD_PATH.shift
|
@@ -0,0 +1,714 @@
|
|
1
|
+
require 'active_support/core_ext/hash/deep_dup.rb'
|
2
|
+
|
3
|
+
module Delineate
|
4
|
+
|
5
|
+
module AttributeMap
|
6
|
+
|
7
|
+
# == Attribute Maps
|
8
|
+
#
|
9
|
+
# The AttributeMap class provides the ability to expose an ActiveRecord model's
|
10
|
+
# attributes and associations in a customized way. By speciying an attribute map,
|
11
|
+
# the model's internal attributes and associations can be de-coupled from its
|
12
|
+
# presentation or interface, allowing a consumer's interaction with the model to
|
13
|
+
# remain consistent even if the model implementation or schema changes.
|
14
|
+
#
|
15
|
+
# Note: Although this description contemplates usage of an attribute map in
|
16
|
+
# terms of defining an API, multiple attribute maps can be constructed
|
17
|
+
# exposing different model interfaces for various use cases.
|
18
|
+
#
|
19
|
+
# To define an attribute map in an ActiveRecord model, do the following:
|
20
|
+
#
|
21
|
+
# class Account < ActiveRecord::Base
|
22
|
+
# map_attributes :api do
|
23
|
+
# attribute :name
|
24
|
+
# attribute :path, :access => :ro
|
25
|
+
# attribute :active, :access => :rw, :using => :active_flag
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# The map_attributes class method establishes an attribute map that
|
30
|
+
# will be used by the model's <map-name>_attributes and <map-name>_attributes= methods.
|
31
|
+
# This map specifies the atrribute names, access permissions, and other options
|
32
|
+
# as viewed by a user of the model's public API. In the example above, 3 of the
|
33
|
+
# the model's attributes are exposed through the API.
|
34
|
+
#
|
35
|
+
# === Mapping Model Attributes
|
36
|
+
#
|
37
|
+
# To declare a model attribute be included in the map, you use the attribute
|
38
|
+
# method on the AttributeMap instance:
|
39
|
+
#
|
40
|
+
# attribute :public_name, :access => :rw, :using => :internal_name
|
41
|
+
#
|
42
|
+
# The first parameter is required and is the map-specific public name for the
|
43
|
+
# attribute. If the :using parameter is not provided, the external name
|
44
|
+
# and internal name are assumed to be identical. If :using is specified,
|
45
|
+
# the name provided must be either an existing model attribute, or a method
|
46
|
+
# that will be called when reading/writing the attribute. In the example above,
|
47
|
+
# if internal_name is not a model attribute, you must define methods
|
48
|
+
# internal_name() and internal_name=(value) in the ActiveRecord class, the
|
49
|
+
# latter being required if the attribute is not read-only.
|
50
|
+
#
|
51
|
+
# The :access parameter can take the following values:
|
52
|
+
#
|
53
|
+
# :rw This value, which is the default, means that the attribute is read-write.
|
54
|
+
# :ro The :ro value designates the attribute as read-only. Attempts to set the
|
55
|
+
# attribute's value will silently fail.
|
56
|
+
# :w The attribute value can be set, but does not appear when the attributes
|
57
|
+
# read.
|
58
|
+
# :none Use this option when merging in a map to ignore the attribute defined in
|
59
|
+
# the other map.
|
60
|
+
#
|
61
|
+
# The :optional parameter affects the reading of a model attribute:
|
62
|
+
#
|
63
|
+
# attribute :balance, :access => :ro, :optional => true
|
64
|
+
#
|
65
|
+
# Optional attributes are not accessed/included when retrieving the mapped
|
66
|
+
# attributes, unless explicitly requested. This can be useful when there are
|
67
|
+
# performance implications for getting an attribute's value, for example. You
|
68
|
+
# can specify a symbol as the value for :optional instead of true. The symbol
|
69
|
+
# then groups together all attributes with that option group. For example, if
|
70
|
+
# you specify:
|
71
|
+
#
|
72
|
+
# attribute :balance, :access => :ro, :optional => :compute_balances
|
73
|
+
# attribute :total_balance, :access => :ro, :optional => :compute_balances
|
74
|
+
#
|
75
|
+
# you then get:
|
76
|
+
#
|
77
|
+
# acct.api_attributes(:include => :balance) # :balance attribute is included in result
|
78
|
+
# acct.api_attributes(:include => :compute_balances) # Both :balance and :total_balance attributes are returned
|
79
|
+
#
|
80
|
+
# The :read and :write parameters are used to define accessor methods for
|
81
|
+
# the attribute. The specified lambda will be defined as a method named
|
82
|
+
# by the :model_attr parameter. For example:
|
83
|
+
#
|
84
|
+
# attribute :parent,
|
85
|
+
# :read => lambda {|a| a.parent ? a.parent.path : nil},
|
86
|
+
# :write => lambda {|a, v| a.parent = {:path => v}}
|
87
|
+
#
|
88
|
+
# Two methods, parent_<map-name>() and parent_<map_name>=(value) will be defined
|
89
|
+
# on the ActiveRecord model.
|
90
|
+
#
|
91
|
+
# === Mapping Model Associations
|
92
|
+
#
|
93
|
+
# In addition to model attributes, you can specify a model's associations in
|
94
|
+
# an attribute map. For example:
|
95
|
+
#
|
96
|
+
# class Account < ActiveRecord::Base
|
97
|
+
# :belongs_to :account_type
|
98
|
+
# map_attributes :api do
|
99
|
+
# attribute :name
|
100
|
+
# attribute :path, :access => :ro
|
101
|
+
# association :type, :using => :account_type
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# The first parameter in the association specification is its mapped name, and
|
106
|
+
# the optional :using parameter is the internal association name. In the
|
107
|
+
# the example above the account_type association is exposed as a nested object
|
108
|
+
# named 'type'.
|
109
|
+
#
|
110
|
+
# When specifying an association mapping, by default the attribute map in
|
111
|
+
# the association's model class is used to define its attributes and nested
|
112
|
+
# associations. If you include an attribute defininiton in the association map,
|
113
|
+
# it will override the spec in the association model:
|
114
|
+
#
|
115
|
+
# class Account < ActiveRecord::Base
|
116
|
+
# :belongs_to :account_type
|
117
|
+
# map_attributes :api do
|
118
|
+
# association :type, :using => :account_type do
|
119
|
+
# attribute :name, :access => :ro
|
120
|
+
# attribute :description, :access => :ro
|
121
|
+
# end
|
122
|
+
# end
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# In this example, if the AccountType attribute map declared :name as
|
126
|
+
# read-write, the association map in the Account model overrides that to make :name
|
127
|
+
# read-only when accessed as a nested object from an Account model. If the
|
128
|
+
# :description attribute of AccountType had not been specified in the AccountType
|
129
|
+
# attribute map, the inclusion of it here lets that attribute be exposed in the
|
130
|
+
# Account attribute map. Note that when overriding an association's attribute, the
|
131
|
+
# override must completely re-define the attribute's options.
|
132
|
+
#
|
133
|
+
# If you want to fully specify an association's attributes, use the
|
134
|
+
# :override option as follows:
|
135
|
+
#
|
136
|
+
# class Account < ActiveRecord::Base
|
137
|
+
# :belongs_to :account_type
|
138
|
+
# map_api_attributes :account do
|
139
|
+
# association :type, :using => :account_type, :override => :replace do
|
140
|
+
# attribute :name, :access => :ro
|
141
|
+
# attribute :description, :access => :ro
|
142
|
+
# association :category, :access => :ro :using => :account_category
|
143
|
+
# attribute :name
|
144
|
+
# end
|
145
|
+
# end
|
146
|
+
# end
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# which re-defines the mapped association as viewed by Account; no merging is
|
150
|
+
# done with the attribute map defined in the AccountType model. In the example
|
151
|
+
# above, note the ability to nest associations. For this to work, account_category
|
152
|
+
# must be declared as an ActiveRecord association in the AccountType class.
|
153
|
+
#
|
154
|
+
# Other parameters for mapping associations:
|
155
|
+
#
|
156
|
+
# :access As with attributes, an association can be declared :ro or :rw (the
|
157
|
+
# default). An association that is writeable will be automatically
|
158
|
+
# specified in an accepts_nested_attributes_for, which allows
|
159
|
+
# attribute writes to contain a nested hash for the association
|
160
|
+
# (except for individual association attributes that are read-only).
|
161
|
+
#
|
162
|
+
# :optional When set to true, the association is not included by default when
|
163
|
+
# retrieving/returning the model's mapped attributes.
|
164
|
+
#
|
165
|
+
# :polymorphic Affects reading only and is relevant when the association class
|
166
|
+
# is an STI base class. When set to true, the attribute map of
|
167
|
+
# each association record (as defined by its class) is used to
|
168
|
+
# specify its included attributes and associations. This means that
|
169
|
+
# in a collection association, the returned attribute hashes may be
|
170
|
+
# heterogeneous, i.e. vary according to each retrieved record's
|
171
|
+
# class. NOTE: when using :polymorphic, you cannot merge/override
|
172
|
+
# the association class attribute map.
|
173
|
+
#
|
174
|
+
# === STI Attribute Maps
|
175
|
+
#
|
176
|
+
# ActiveRecord STI subclasses inherit the attribute maps from their superclass.
|
177
|
+
# If you want to include additional subclass attributes, just invoke
|
178
|
+
# map_attributes in the subclass and define the extra attributes and
|
179
|
+
# associations. If the subclass wants to completely override/replace the
|
180
|
+
# superclass map, do:
|
181
|
+
#
|
182
|
+
# class MySubclass < MyBase
|
183
|
+
# map_attributes :api, :override => :replace do
|
184
|
+
# .
|
185
|
+
# .
|
186
|
+
# end
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
class AttributeMap
|
190
|
+
attr_reader :klass_name
|
191
|
+
attr_reader :name
|
192
|
+
attr_accessor :attributes
|
193
|
+
attr_accessor :associations
|
194
|
+
|
195
|
+
# The klass constructor parameter is the ActiveRecord model class for
|
196
|
+
# the map being created.
|
197
|
+
def initialize(klass_name, name, options = {})
|
198
|
+
@klass_name = klass_name
|
199
|
+
@name = name
|
200
|
+
@options = options
|
201
|
+
validate_map_options(options)
|
202
|
+
|
203
|
+
@attributes = {}
|
204
|
+
@associations = {}
|
205
|
+
@write_attributes = {:_destroy => :_destroy}
|
206
|
+
@resolved = false
|
207
|
+
@sti_baseclass_merged = false
|
208
|
+
end
|
209
|
+
|
210
|
+
# Declare a single attribute to be included in the map. You can declare a list,
|
211
|
+
# but the attribute options are limited to :access and :optional.
|
212
|
+
def attribute(*args)
|
213
|
+
options = args.extract_options!
|
214
|
+
validate_attribute_options(options, args.size)
|
215
|
+
|
216
|
+
args.each do |name|
|
217
|
+
if options[:access] == :none
|
218
|
+
@attributes.delete(name)
|
219
|
+
@write_attributes.delete(name)
|
220
|
+
else
|
221
|
+
@attributes[name] = options
|
222
|
+
|
223
|
+
model_attr = (options[:model_attr] || name).to_sym
|
224
|
+
model_attr = define_attr_methods(name, model_attr, options) unless is_model_attr?(model_attr)
|
225
|
+
|
226
|
+
if options[:access] != :ro
|
227
|
+
if model_attr.to_s != klass.primary_key && !klass.accessible_attributes.detect { |a| a == model_attr.to_s }
|
228
|
+
raise "Expected 'attr_accessible :#{model_attr}' in #{@klass_name}"
|
229
|
+
end
|
230
|
+
@write_attributes[name] = model_attr
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Declare an association to be included in the map.
|
237
|
+
def association(name, options = {}, &blk)
|
238
|
+
validate_association_options(options, block_given?)
|
239
|
+
|
240
|
+
model_attr = (options[:model_attr] || name).to_sym
|
241
|
+
reflection = get_model_association(model_attr)
|
242
|
+
|
243
|
+
attr_map = options.delete(:attr_map) || AttributeMap.new(reflection.class_name, @name)
|
244
|
+
attr_map.instance_variable_set(:@options, {:override => options[:override]}) if options[:override]
|
245
|
+
|
246
|
+
attr_map.instance_eval(&blk) if block_given?
|
247
|
+
|
248
|
+
if !merge_option?(options) && attr_map.empty?
|
249
|
+
raise ArgumentError, "Map association '#{name}' in class #{@klass_name} specifies :replace but has empty block"
|
250
|
+
end
|
251
|
+
if options[:access] != :ro and !klass.accessible_attributes.include?(model_attr.to_s+'_attributes')
|
252
|
+
raise "Expected attr_accessible and/or accepts_nested_attributes_for :#{model_attr} in #{@klass_name} model"
|
253
|
+
end
|
254
|
+
|
255
|
+
@associations[name] = {:klass_name => reflection.class_name, :options => options,
|
256
|
+
:attr_map => attr_map.empty? ? nil : attr_map,
|
257
|
+
:collection => (reflection.macro == :has_many || reflection.macro == :has_and_belongs_to_many)}
|
258
|
+
end
|
259
|
+
|
260
|
+
# Returns a schema hash according to the attribute map. This information
|
261
|
+
# could be used to generate clients.
|
262
|
+
#
|
263
|
+
# The schema hash has two keys: +attributes+ and +associations+. The content
|
264
|
+
# for each varies depeding on the +access+ parameter which can take values
|
265
|
+
# of :read, :write, or nil. The +attributes+ hash looks like this:
|
266
|
+
#
|
267
|
+
# :read or :write { :name => :string, :age => :integer }
|
268
|
+
# :nil { :name => {:type => :string, :access => :rw}, :age => { :type => :integer, :access => :rw} }
|
269
|
+
#
|
270
|
+
# The +associations+ hash looks like this:
|
271
|
+
#
|
272
|
+
# :read or :write { :posts => {}, :comments => {:optional => true} }
|
273
|
+
# nil { :posts => {:access => :rw}, :comments => {:optional => true, :access=>:ro} }
|
274
|
+
#
|
275
|
+
# This method uses the +columns_hash+ provided by ActiveRecord. You can implement
|
276
|
+
# that method in your custom models if you want to customize the schema output.
|
277
|
+
#
|
278
|
+
def schema(access = nil, schemas = [])
|
279
|
+
schemas.push(@klass_name)
|
280
|
+
resolve
|
281
|
+
|
282
|
+
columns = (klass_cti_subclass? ? klass.cti_base_class.columns_hash : {}).merge klass.columns_hash
|
283
|
+
attrs = {}
|
284
|
+
@attributes.each do |attr, opts|
|
285
|
+
attr_type = (column = columns[model_attribute(attr).to_s]) ? column.type : nil
|
286
|
+
if (access == :read && opts[:access] != :w) or (access == :write && opts[:access] != :ro)
|
287
|
+
attrs[attr] = attr_type
|
288
|
+
elsif access.nil?
|
289
|
+
attrs[attr] = {:type => attr_type, :access => opts[:access] || :rw}
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
associations = {}
|
294
|
+
@associations.each do |assoc_name, assoc|
|
295
|
+
include_assoc = (access == :read && assoc[:options][:access] != :w) || (access == :write && assoc[:options][:access] != :ro) || access.nil?
|
296
|
+
if include_assoc
|
297
|
+
associations[assoc_name] = {}
|
298
|
+
associations[assoc_name][:optional] = true if assoc[:options][:optional]
|
299
|
+
end
|
300
|
+
|
301
|
+
associations[assoc_name][:access] = (assoc[:options][:access] || :rw) if access.nil?
|
302
|
+
|
303
|
+
if include_assoc && assoc[:attr_map] && assoc[:attr_map] != assoc[:klass_name].to_s.constantize.attribute_map(@name)
|
304
|
+
associations[assoc_name].merge! assoc[:attr_map].schema(access, schemas) unless schemas.include?(assoc[:klass_name])
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
schemas.pop
|
309
|
+
{:attributes => attrs, :associations => associations}
|
310
|
+
end
|
311
|
+
|
312
|
+
def resolved?
|
313
|
+
@resolved
|
314
|
+
end
|
315
|
+
|
316
|
+
# Will raise an exception of the map cannot be fully resolved
|
317
|
+
def resolve!
|
318
|
+
resolve(:must_resolve)
|
319
|
+
self
|
320
|
+
end
|
321
|
+
|
322
|
+
# Attempts to resolve the map and the maps it depends on. If must_resolve is truthy, will
|
323
|
+
# raise an exception if map cannot be resolved.
|
324
|
+
def resolve(must_resolve = false, resolving = [])
|
325
|
+
return true if @resolved
|
326
|
+
return true if resolving.include?(@klass_name) # prevent infinite recursion
|
327
|
+
|
328
|
+
resolving.push(@klass_name)
|
329
|
+
|
330
|
+
result = resolve_associations(must_resolve, resolving)
|
331
|
+
result = false unless resolve_sti_baseclass(must_resolve, resolving)
|
332
|
+
|
333
|
+
resolving.pop
|
334
|
+
@resolved = result
|
335
|
+
end
|
336
|
+
|
337
|
+
# Values for includes param:
|
338
|
+
# nil = include all attributes
|
339
|
+
# [] = do not include optional attributes
|
340
|
+
# [...] = include the specified optional attributes
|
341
|
+
def serializable_attribute_names(includes = nil)
|
342
|
+
attribute_names = @attributes.keys.reject {|k| @attributes[k][:access] == :w}
|
343
|
+
return attribute_names if includes.nil?
|
344
|
+
|
345
|
+
attribute_names.delete_if do |key|
|
346
|
+
(option = @attributes[key][:optional]) && !includes.include?(key) && !includes.include?(option)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def serializable_association_names(includes = nil)
|
351
|
+
return @associations.keys if includes.nil?
|
352
|
+
|
353
|
+
@associations.inject([]) do |assoc_names, assoc|
|
354
|
+
assoc_names << assoc.first if !(option = assoc.last[:options][:optional]) || includes.include?(assoc.first) || includes.include?(option)
|
355
|
+
assoc_names
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
# Given the specified api attributes hash, translates the attribute names to
|
360
|
+
# the corresponding model attribute names. Recursive translation on associations
|
361
|
+
# is performed. API attributes that are defined as read-only are removed.
|
362
|
+
#
|
363
|
+
# Input can be a single hash or an array of hashes.
|
364
|
+
def map_attributes_for_write(attrs, options = nil)
|
365
|
+
raise "Cannot process map #{@klass_name}:#{@name} for write because it has not been resolved" if !resolve
|
366
|
+
|
367
|
+
(attrs.is_a?(Array) ? attrs : [attrs]).each do |attr_hash|
|
368
|
+
raise ArgumentError, "Expected attributes hash but received #{attr_hash.inspect}" if !attr_hash.is_a?(Hash)
|
369
|
+
|
370
|
+
attr_hash.dup.symbolize_keys.each do |k, v|
|
371
|
+
if assoc = @associations[k]
|
372
|
+
map_association_attributes_for_write(assoc, attr_hash, k)
|
373
|
+
else
|
374
|
+
if @write_attributes.has_key?(k)
|
375
|
+
attr_hash.rename_key!(k, @write_attributes[k]) if @write_attributes[k] != k
|
376
|
+
else
|
377
|
+
attr_hash.delete(k)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
attrs
|
384
|
+
end
|
385
|
+
|
386
|
+
def attribute_value(record, name)
|
387
|
+
model_attr = model_attribute(name)
|
388
|
+
model_attr == :type ? record.read_attribute(:type) : record.send(model_attr)
|
389
|
+
end
|
390
|
+
|
391
|
+
def model_association(name)
|
392
|
+
@associations[name][:options][:model_attr] || name
|
393
|
+
end
|
394
|
+
|
395
|
+
# Access the map of an association defined in this map. Will throw an
|
396
|
+
# error if the map cannot be found and resolved.
|
397
|
+
def association_attribute_map(association)
|
398
|
+
assoc = @associations[association]
|
399
|
+
validate(assoc_attr_map(assoc), assoc[:klass_name])
|
400
|
+
assoc_attr_map(assoc)
|
401
|
+
end
|
402
|
+
|
403
|
+
def validate(map, class_name)
|
404
|
+
raise(NameError, "Expected attribute map :#{@name} to be defined for class '#{class_name}'") if map.nil?
|
405
|
+
map.resolve! unless map.resolved?
|
406
|
+
map
|
407
|
+
end
|
408
|
+
|
409
|
+
# Merges another AttributeMap instance into this instance.
|
410
|
+
def merge!(other_attr_map, merge_opts = {})
|
411
|
+
return if other_attr_map.nil?
|
412
|
+
|
413
|
+
@attributes = @attributes.deep_merge(other_attr_map.attributes)
|
414
|
+
@associations.deep_merge!(other_attr_map.associations)
|
415
|
+
|
416
|
+
@write_attributes = {:_destroy => :_destroy}
|
417
|
+
@attributes.each {|k, v| @write_attributes[k] = (v[:model_attr] || k) unless v[:access] == :ro}
|
418
|
+
|
419
|
+
@options = other_attr_map.instance_variable_get(:@options).dup if merge_opts[:with_options]
|
420
|
+
@resolved = other_attr_map.resolved? if merge_opts[:with_state]
|
421
|
+
|
422
|
+
self
|
423
|
+
end
|
424
|
+
|
425
|
+
# Returns a new copy of this AttributeMap instance
|
426
|
+
def dup
|
427
|
+
returning self.class.new(@klass_name, @name) do |map|
|
428
|
+
map.attributes = @attributes.dup
|
429
|
+
map.instance_variable_set(:@write_attributes, @write_attributes.dup)
|
430
|
+
map.associations = @associations.dup
|
431
|
+
|
432
|
+
map.instance_variable_set(:@resolved, @resolved)
|
433
|
+
map.instance_variable_set(:@sti_baseclass_merged, @sti_baseclass_merged)
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
def copy(other_map)
|
438
|
+
@attributes = other_map.attributes.deep_dup
|
439
|
+
@write_attributes = other_map.instance_variable_get(:@write_attributes).deep_dup
|
440
|
+
@associations = other_map.associations.deep_dup
|
441
|
+
|
442
|
+
@resolved = other_map.instance_variable_get(:@resolved)
|
443
|
+
@sti_baseclass_merged = other_map.instance_variable_get(:@sti_baseclass_merged)
|
444
|
+
|
445
|
+
self
|
446
|
+
end
|
447
|
+
|
448
|
+
|
449
|
+
protected
|
450
|
+
|
451
|
+
def klass
|
452
|
+
@klass ||= @klass_name.constantize
|
453
|
+
end
|
454
|
+
|
455
|
+
def empty?
|
456
|
+
@attributes.empty? && @associations.empty?
|
457
|
+
end
|
458
|
+
|
459
|
+
def model_attribute(name)
|
460
|
+
@attributes[name][:model_attr] || name
|
461
|
+
end
|
462
|
+
|
463
|
+
def assoc_attr_map(assoc)
|
464
|
+
assoc[:attr_map] || assoc[:klass_name].constantize.attribute_map(@name)
|
465
|
+
end
|
466
|
+
|
467
|
+
# Map an association's attributes for writing. Will call
|
468
|
+
# map_attributes_for_write (resulting in recursion) on the association
|
469
|
+
# if it's a has_one or belongs_to, or calls map_attributes_for_write
|
470
|
+
# on each element of a has_many collection.
|
471
|
+
def map_association_attributes_for_write(assoc, attr_hash, key)
|
472
|
+
if assoc[:options][:access] == :ro
|
473
|
+
attr_hash.delete(key) # Writes not allowed
|
474
|
+
else
|
475
|
+
assoc_attrs = attr_hash[key]
|
476
|
+
if assoc[:collection]
|
477
|
+
attr_hash[key] = xlate_params_for_nested_attributes_collection(assoc_attrs)
|
478
|
+
|
479
|
+
# Iterate thru each element in the collection and map its attributes
|
480
|
+
attr_hash[key].each do |entry_attrs|
|
481
|
+
entry_attrs = entry_attrs[1] if entry_attrs.is_a?(Array)
|
482
|
+
assoc_attr_map(assoc).map_attributes_for_write(entry_attrs)
|
483
|
+
end
|
484
|
+
else
|
485
|
+
# Association is a one-to-one; map its attributes
|
486
|
+
assoc_attr_map(assoc).map_attributes_for_write(assoc_attrs)
|
487
|
+
end
|
488
|
+
|
489
|
+
model_attr = assoc[:options][:model_attr] || key
|
490
|
+
attr_hash[(model_attr.to_s + '_attributes').to_sym] = attr_hash.delete(key)
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
VALID_ASSOC_OPTIONS = [ :model_attr, :using, :override, :polymorphic, :access, :optional, :attr_map ]
|
495
|
+
|
496
|
+
def validate_association_options(options, blk)
|
497
|
+
options.assert_valid_keys(VALID_ASSOC_OPTIONS)
|
498
|
+
validate_access_option(options[:access])
|
499
|
+
options[:model_attr] = options.delete(:using) if options.key?(:using)
|
500
|
+
|
501
|
+
raise ArgumentError, 'Cannot specify :override or provide block with :polymorphic' if options[:polymorphic] and (blk or options[:override])
|
502
|
+
raise ArgumentError, 'Option :override must be :replace or :merge' unless !options.key?(:override) || [:merge, :replace].include?(options[:override])
|
503
|
+
end
|
504
|
+
|
505
|
+
VALID_ATTR_OPTIONS = [ :model_attr, :access, :optional, :read, :write, :using ]
|
506
|
+
VALID_ATTR_OPTIONS_MULTIPLE = [ :access, :optional ]
|
507
|
+
|
508
|
+
def validate_attribute_options(options, arg_count = 1)
|
509
|
+
options.assert_valid_keys(VALID_ATTR_OPTIONS) if arg_count == 1
|
510
|
+
options.assert_valid_keys(VALID_ATTR_OPTIONS_MULTIPLE) if arg_count > 1
|
511
|
+
|
512
|
+
options[:model_attr] = options.delete(:using) if options.key?(:using)
|
513
|
+
options[:access] = :rw if !options.key?(:access)
|
514
|
+
|
515
|
+
validate_access_option(options[:access])
|
516
|
+
raise ArgumentError, 'Cannot specify :write option for read-only attribute' if options[:access] == :ro && options[:write]
|
517
|
+
end
|
518
|
+
|
519
|
+
VALID_MAP_OPTIONS = [ :override, :no_primary_key_attr, :no_destroy_attr ]
|
520
|
+
|
521
|
+
def validate_map_options(options)
|
522
|
+
options.assert_valid_keys(VALID_MAP_OPTIONS)
|
523
|
+
raise ArgumentError, 'Option :override must be :replace or :merge' unless !options.key?(:override) || [:merge, :replace].include?(options[:override])
|
524
|
+
if options[:override] == :replace && klass.descends_from_active_record? && !klass_cti_subclass?
|
525
|
+
raise ArgumentError, "Cannot specify :override => :replace in map_attributes for #{@klass_name} unless it is a CTI or STI subclass"
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
def validate_access_option(opt)
|
530
|
+
raise ArgumentError, 'Invalid value for :access option' if opt and ![:ro, :rw, :w, :none].include?(opt)
|
531
|
+
end
|
532
|
+
|
533
|
+
def get_model_association(association)
|
534
|
+
returning association_reflection(association) do |reflection|
|
535
|
+
raise ArgumentError, "Association '#{association}' in model #{@klass_name} is not defined yet" if reflection.nil?
|
536
|
+
begin
|
537
|
+
reflection.klass
|
538
|
+
rescue
|
539
|
+
raise NameError, "Cannot resolve association class '#{reflection.class_name}' from model '#{@klass_name}'"
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
def association_reflection(model_assoc)
|
545
|
+
reflection = klass.reflect_on_association(model_assoc)
|
546
|
+
reflection || (klass.cti_base_class.reflect_on_association(model_assoc) if klass_cti_subclass?)
|
547
|
+
end
|
548
|
+
|
549
|
+
def is_model_attr?(name)
|
550
|
+
klass.column_names.include?(name.to_s)
|
551
|
+
end
|
552
|
+
|
553
|
+
def merge_option?(options)
|
554
|
+
options[:override] != :replace
|
555
|
+
end
|
556
|
+
|
557
|
+
def resolve_associations(must_resolve, resolving)
|
558
|
+
result = true
|
559
|
+
|
560
|
+
@associations.each do |assoc_name, assoc|
|
561
|
+
if detect_circular_merge(assoc)
|
562
|
+
raise "Detected attribute map circular merge references: class=#{@klass_name}, association=#{assoc_name}"
|
563
|
+
end
|
564
|
+
|
565
|
+
assoc_map = assoc[:attr_map] || assoc[:klass_name].constantize.attribute_maps.try(:fetch, @name, nil)
|
566
|
+
if assoc_map && !assoc_map.resolved?
|
567
|
+
if assoc_map.resolve(must_resolve, resolving) && merge_option?(assoc[:options]) && assoc[:attr_map]
|
568
|
+
merge_map = assoc[:klass_name].constantize.attribute_maps[@name]
|
569
|
+
assoc_map = merge_map.dup.merge!(assoc_map, :with_options => true, :with_state => true)
|
570
|
+
end
|
571
|
+
end
|
572
|
+
assoc[:attr_map] = assoc_map
|
573
|
+
|
574
|
+
if assoc_map.nil? or !assoc_map.resolve(false, resolving)
|
575
|
+
result = false
|
576
|
+
raise "Cannot resolve map for association :#{assoc_name} in #{@klass_name}:#{@name} map" if must_resolve
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
result
|
581
|
+
end
|
582
|
+
|
583
|
+
# If this is the map of an STI subclass, inherit/merge the map from the base class
|
584
|
+
def resolve_sti_baseclass(must_resolve, resolving)
|
585
|
+
result = true
|
586
|
+
|
587
|
+
if !klass.descends_from_active_record? && !@sti_baseclass_merged && result && @options[:override] != :replace
|
588
|
+
if klass.superclass.attribute_maps.try(:fetch, @name, nil).try(:resolve, must_resolve, resolving)
|
589
|
+
@resolved = @sti_baseclass_merged = true
|
590
|
+
self.copy(klass.superclass.attribute_maps[@name].dup.merge!(self))
|
591
|
+
else
|
592
|
+
result = false
|
593
|
+
raise "Can't resolve base class map for #{@klass_name}:#{@name} map" if must_resolve
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
result
|
598
|
+
end
|
599
|
+
|
600
|
+
def klass_cti_subclass?
|
601
|
+
klass.respond_to?(:is_cti_subclass) && klass.is_cti_subclass?
|
602
|
+
end
|
603
|
+
|
604
|
+
# Checks to see if an assocation specifies a merge, and the association class's
|
605
|
+
# attribute map attempts to merge the association parent attribute map.
|
606
|
+
def detect_circular_merge(assoc)
|
607
|
+
return if assoc.nil? || assoc[:attr_map].nil? || !merge_option?(assoc[:options])
|
608
|
+
return unless (map = assoc[:klass_name].constantize.attribute_maps.try(:fetch, @name, nil))
|
609
|
+
|
610
|
+
map.associations.each_value do |a|
|
611
|
+
return true if a[:klass_name] == @klass_name && merge_option?(a[:options]) && a[:attr_map]
|
612
|
+
end
|
613
|
+
|
614
|
+
false
|
615
|
+
end
|
616
|
+
|
617
|
+
# Defines custom read/write attribute methods
|
618
|
+
def define_attr_methods(name, model_attr, options)
|
619
|
+
read_model_attr = define_attr_reader_method(name, model_attr, options)
|
620
|
+
write_model_attr = define_attr_writer_method(name, model_attr, options)
|
621
|
+
|
622
|
+
if read_model_attr || write_model_attr
|
623
|
+
options[:model_attr] = read_model_attr || write_model_attr
|
624
|
+
else
|
625
|
+
model_attr
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
def define_attr_reader_method(name, model_attr, options)
|
630
|
+
return unless (reader = options[:read])
|
631
|
+
raise ArgumentError, 'Invalid parameter for :read' unless (reader.is_a?(Symbol) || reader.is_a?(Proc))
|
632
|
+
|
633
|
+
returning(model_attr == name ? "#{name}_#{@name}" : model_attr) do |method_name|
|
634
|
+
if reader.is_a?(Symbol)
|
635
|
+
klass.class_eval %(
|
636
|
+
def #{method_name}
|
637
|
+
if @serializer_context
|
638
|
+
@serializer_context.send(attribute_map(:#{@name}).attributes[:#{name}][:read], self)
|
639
|
+
else
|
640
|
+
self.send(attribute_map(:#{@name}).attributes[:#{name}][:read])
|
641
|
+
end
|
642
|
+
end
|
643
|
+
), __FILE__, __LINE__
|
644
|
+
else
|
645
|
+
klass.class_eval %(
|
646
|
+
def #{method_name}
|
647
|
+
attribute_map(:#{@name}).attributes[:#{name}][:read].call(self)
|
648
|
+
end
|
649
|
+
), __FILE__, __LINE__
|
650
|
+
end
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
def define_attr_writer_method(name, model_attr, options)
|
655
|
+
return unless (writer = options[:write])
|
656
|
+
raise ArgumentError, 'Invalid parameter for :write' unless (writer.is_a?(Symbol) || writer.is_a?(Proc))
|
657
|
+
|
658
|
+
returning(model_attr == name ? "#{name}_#{@name}" : model_attr) do |method_name|
|
659
|
+
if writer.is_a?(Symbol)
|
660
|
+
klass.class_eval %(
|
661
|
+
def #{method_name}=(value)
|
662
|
+
if @serializer_context
|
663
|
+
@serializer_context.send(attribute_map(:#{@name}).attributes[:#{name}][:write], self, value)
|
664
|
+
else
|
665
|
+
self.send(attribute_map(:#{@name}).attributes[:#{name}][:write], value)
|
666
|
+
end
|
667
|
+
end
|
668
|
+
), __FILE__, __LINE__
|
669
|
+
else
|
670
|
+
klass.class_eval %(
|
671
|
+
def #{method_name}=(value)
|
672
|
+
attribute_map(:#{@name}).attributes[:#{name}][:write].call(self, value)
|
673
|
+
end
|
674
|
+
), __FILE__, __LINE__
|
675
|
+
end
|
676
|
+
|
677
|
+
klass.attr_accessible method_name
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
# The params hash generated from XML/JSON needs to be translated to a form
|
682
|
+
# compatible with ActiveRecord nested attributes, specifically with respect
|
683
|
+
# to association collections. For example, when the XML input is:
|
684
|
+
#
|
685
|
+
# <entries>
|
686
|
+
# <entry>
|
687
|
+
# ... entry 1 stuff ...
|
688
|
+
# </entry>
|
689
|
+
# <entry>
|
690
|
+
# ... entry 2 stuff ...
|
691
|
+
# </entry>
|
692
|
+
# </entries>
|
693
|
+
#
|
694
|
+
# Rails constructs the resulting params hash as:
|
695
|
+
#
|
696
|
+
# {"entries"=>{"entry"=>[{... entry 1 stuff...}, {... entry 2 stuff...}]}}
|
697
|
+
#
|
698
|
+
# which is incompatible with ActiveRecord nested attrributes. So this method
|
699
|
+
# detects that pattern, and translates the above to:
|
700
|
+
#
|
701
|
+
# {"entries"=> [{... entry 1 stuff...}, {... entry 2 stuff...}]}
|
702
|
+
#
|
703
|
+
def xlate_params_for_nested_attributes_collection(assoc_attrs)
|
704
|
+
if assoc_attrs.is_a?(Hash) and assoc_attrs.keys.size == 1 and assoc_attrs[assoc_attrs.keys.first].is_a?(Array)
|
705
|
+
assoc_attrs[assoc_attrs.keys.first]
|
706
|
+
else
|
707
|
+
assoc_attrs
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
711
|
+
end
|
712
|
+
|
713
|
+
end
|
714
|
+
end
|