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
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module Delineate
|
4
|
+
module AttributeMap
|
5
|
+
|
6
|
+
# AttributeMap serializer that handles CSV as the external data format.
|
7
|
+
class CsvSerializer < MapSerializer
|
8
|
+
|
9
|
+
# Returns the record's mapped attributes as a CSV string. If
|
10
|
+
# you specify a truthy value for the :include_header option,
|
11
|
+
# the CSV header is output as the first line.
|
12
|
+
#
|
13
|
+
# See the description in +serializable_record+ for the order
|
14
|
+
# of the attributes.
|
15
|
+
def serialize(options = {})
|
16
|
+
opts = options[:include_header] ?
|
17
|
+
{:write_headers => true, :headers => serializable_header, :encoding => "UTF-8"} :
|
18
|
+
{:encoding => "UTF-8"}
|
19
|
+
|
20
|
+
opts = remove_serializer_class_options(options).merge(opts)
|
21
|
+
opts.delete(:include_header)
|
22
|
+
|
23
|
+
CSV.generate(opts) do |csv|
|
24
|
+
serializable_record.each {|r| csv << r}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the header row as a CSV string.
|
29
|
+
def serialize_header(options = {})
|
30
|
+
opts = {:encoding => "UTF-8"}.merge(remove_serializer_class_options(options))
|
31
|
+
CSV.generate_line(serializable_header, opts)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Not implemented yet.
|
35
|
+
def serialize_in(csv_string, options = {})
|
36
|
+
raise "Serializing from CSV is not supported at this time. You can inherit a class from CsvSerializer to write a custom importer."
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the record's mapped attributes in the serializer's "internal"
|
40
|
+
# format. For this class the representation is an array of one or more
|
41
|
+
# rows, one row for each item in teh record's has_many collections. Each
|
42
|
+
# row is an array of values ordered as follows:
|
43
|
+
#
|
44
|
+
# 1. All the record's mapped attributes in map order.
|
45
|
+
# 2. All one-to-one mapped association attributes in map order.
|
46
|
+
# 3. All one-to-many mapped association attributes in map order
|
47
|
+
#
|
48
|
+
# Do not specify any method parameters when calling +serializable_record+.
|
49
|
+
def serializable_record(prefix = [], top_level = true)
|
50
|
+
new_rows = []
|
51
|
+
|
52
|
+
prefix += serializable_attribute_names.map do |name|
|
53
|
+
@attribute_map.attribute_value(@record, name)
|
54
|
+
end
|
55
|
+
|
56
|
+
add_includes(:one_to_one) do |association, record, opts, nil_record|
|
57
|
+
assoc_map = association_attribute_map(association)
|
58
|
+
prefix, new_rows = self.class.new(record, assoc_map, opts).serializable_record(prefix, false)
|
59
|
+
end
|
60
|
+
|
61
|
+
add_includes(:one_to_many) do |association, records, opts, nil_record|
|
62
|
+
assoc_map = association_attribute_map(association)
|
63
|
+
records.each do |r|
|
64
|
+
p, next_rows = self.class.new(r, assoc_map, opts).serializable_record(prefix, false)
|
65
|
+
new_rows << (next_rows.empty? ? p : next_rows) unless nil_record
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
top_level ? (new_rows << prefix if new_rows.empty?; new_rows) : [prefix, new_rows]
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the header row as an array of strings, one for each
|
73
|
+
# mapped attribute, including nested assoications. The items
|
74
|
+
# appear in the array in the same order as their corresponding
|
75
|
+
# attribute values.
|
76
|
+
def serializable_header(prefix = '')
|
77
|
+
returning(serializable_header = serializable_attribute_names) do
|
78
|
+
serializable_header.map! {|h| headerize(prefix + h.to_s)}
|
79
|
+
|
80
|
+
add_includes(:one_to_one) do |association, record, opts|
|
81
|
+
assoc_map = association_attribute_map(association)
|
82
|
+
assoc_prefix = prefix + association.to_s + '_'
|
83
|
+
serializable_header.concat self.class.new(record, assoc_map, opts).serializable_header(assoc_prefix)
|
84
|
+
end
|
85
|
+
|
86
|
+
add_includes(:one_to_many) do |association, records, opts|
|
87
|
+
assoc_map = association_attribute_map(association)
|
88
|
+
assoc_prefix = prefix + association.to_s.singularize + '_'
|
89
|
+
serializable_header.concat self.class.new(records.first, assoc_map, opts).serializable_header(assoc_prefix)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# The diff here is that if the associaton record(s) is empty, we have to generate
|
97
|
+
# a new empty record: @record.class.new.build_xxx or @record.class.new.xxx.build
|
98
|
+
def add_includes(assoc_type)
|
99
|
+
includes = @options.delete(:include)
|
100
|
+
assoc_includes, associations = serializable_association_names(includes)
|
101
|
+
|
102
|
+
for association in associations
|
103
|
+
model_assoc = @attribute_map.model_association(association)
|
104
|
+
|
105
|
+
case reflection(model_assoc).macro
|
106
|
+
when :has_many, :has_and_belongs_to_many
|
107
|
+
next if assoc_type == :one_to_one
|
108
|
+
records = @record.send(model_assoc).to_a
|
109
|
+
records = [@record.class.new.send(model_assoc).build] if (nil_record = records.empty?)
|
110
|
+
when :has_one, :belongs_to
|
111
|
+
next if assoc_type != :one_to_one
|
112
|
+
records = @record.send(model_assoc)
|
113
|
+
records = @record.class.new.send('build_'+model_assoc.to_s) if (nil_record = records.nil?)
|
114
|
+
end
|
115
|
+
|
116
|
+
yield(association, records, assoc_includes.is_a?(Hash) ? assoc_includes[association] : {}, nil_record)
|
117
|
+
end
|
118
|
+
|
119
|
+
@options[:include] = includes if includes
|
120
|
+
end
|
121
|
+
|
122
|
+
def headerize(attribute_name)
|
123
|
+
str = attribute_name.gsub(/_/, " ").gsub(/\b('?[a-z])/) { $1.capitalize }
|
124
|
+
|
125
|
+
words = str.split(' ')
|
126
|
+
str = words[0..-2].join(' ') if words[-2] == words[-1]
|
127
|
+
str
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Delineate
|
2
|
+
module AttributeMap
|
3
|
+
|
4
|
+
# AttributeMap serializer that handles JSON as the external data format.
|
5
|
+
class JsonSerializer < MapSerializer
|
6
|
+
|
7
|
+
# Returns the record's mapped attributes as a JSON string. The specified options
|
8
|
+
# are passed to the JSON.encode() function.
|
9
|
+
def serialize(options = {})
|
10
|
+
hash = super()
|
11
|
+
|
12
|
+
if options[:root] == true
|
13
|
+
hash = { @record.class.model_name.element.to_sym => hash }
|
14
|
+
elsif options[:root]
|
15
|
+
hash = { options[:root].to_sym => hash }
|
16
|
+
end
|
17
|
+
opts = remove_serializer_class_options(options)
|
18
|
+
opts.delete(:root)
|
19
|
+
|
20
|
+
hash.to_json(opts)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Takes a record's attributes represented as a JSON string, and returns a
|
24
|
+
# hash suitable for direct assignment to the record's collection of attributes.
|
25
|
+
# For example:
|
26
|
+
#
|
27
|
+
# s = ActiveRecord::AttributeMap::JsonSerializer.new(record, :api)
|
28
|
+
# record.attributes = s.serialize_in(json_string)
|
29
|
+
#
|
30
|
+
def serialize_in(json_string, options = {})
|
31
|
+
super(ActiveSupport::JSON.decode(json_string), options)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module Delineate #:nodoc:
|
2
|
+
module AttributeMap
|
3
|
+
|
4
|
+
# The MapSerializer class serves as the base class for processing the
|
5
|
+
# reading and writing of ActiveRecord model attributes through an
|
6
|
+
# attribute map. Each serializer class supports its own external format
|
7
|
+
# for the input/output of the attributes. The format handled by MapSerializer
|
8
|
+
# is a hash.
|
9
|
+
class MapSerializer
|
10
|
+
|
11
|
+
# Creates a serializer for a single record.
|
12
|
+
#
|
13
|
+
# The +attribute_map+ parameter can be an AttributeMap instance or the
|
14
|
+
# name of the record's attribute map. The +options+ hash is used to
|
15
|
+
# filter which attributes and associations are to be serialized for
|
16
|
+
# output, and can have the following keys:
|
17
|
+
#
|
18
|
+
# :include Specifies which optional attributes and associations to output.
|
19
|
+
# :only Restricts the attributes and associations to only those specified.
|
20
|
+
# :except Processes attributes and associations except those specified.
|
21
|
+
#
|
22
|
+
# See the description for +mapped_attributes+ for more info about options.
|
23
|
+
#
|
24
|
+
def initialize(record, attribute_map, options = nil)
|
25
|
+
@record = record
|
26
|
+
attribute_map = record.send(:attribute_map, attribute_map) if attribute_map.is_a?(Symbol)
|
27
|
+
@attribute_map = attribute_map
|
28
|
+
@options = options ? options.dup : {}
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the record's mapped attributes in the serializer's intrinsic format.
|
32
|
+
#
|
33
|
+
# For the MapSerializer class the attributes are returned as a hash,
|
34
|
+
# and the +options+ parameter is ignored.
|
35
|
+
def serialize(options = {})
|
36
|
+
@attribute_map.resolve! unless @attribute_map.resolved?
|
37
|
+
serializable_record
|
38
|
+
end
|
39
|
+
|
40
|
+
# Takes a record's attributes in the serializer's intrinsic format, and
|
41
|
+
# returns a hash suitable for direct assignment to the record's collection
|
42
|
+
# of attributes. For example:
|
43
|
+
#
|
44
|
+
# s = ActiveRecord::AttributeMap::MapSerializer.new(record, :api)
|
45
|
+
# record.attributes = s.serialize_in(attrs_hash)
|
46
|
+
#
|
47
|
+
def serialize_in(attributes, options = {})
|
48
|
+
@attribute_map.resolve! unless @attribute_map.resolved?
|
49
|
+
@attribute_map.map_attributes_for_write(attributes, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the record's mapped attributes in the serializer's "internal"
|
53
|
+
# format, usually this is a hash.
|
54
|
+
def serializable_record
|
55
|
+
returning(serializable_record = Hash.new) do
|
56
|
+
serializable_attribute_names.each do |name|
|
57
|
+
serializable_record[name] = @attribute_map.attribute_value(@record, name)
|
58
|
+
end
|
59
|
+
|
60
|
+
add_includes do |association, records, opts|
|
61
|
+
polymorphic = @attribute_map.associations[association][:options][:polymorphic]
|
62
|
+
assoc_map = association_attribute_map(association)
|
63
|
+
|
64
|
+
if records.is_a?(Enumerable)
|
65
|
+
serializable_record[association] = records.collect do |r|
|
66
|
+
assoc_map = attribute_map_for_record(r) if polymorphic
|
67
|
+
self.class.new(r, assoc_map, opts).serializable_record
|
68
|
+
end
|
69
|
+
else
|
70
|
+
assoc_map = attribute_map_for_record(records) if polymorphic
|
71
|
+
serializable_record[association] = self.class.new(records, assoc_map, opts).serializable_record
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
# Returns the list of mapped attribute names that are to be output
|
80
|
+
# by applying the serializer's +:include+, +:only+, and +:except+
|
81
|
+
# options to the attribute map.
|
82
|
+
def serializable_attribute_names
|
83
|
+
includes = @options[:include] || []
|
84
|
+
includes = [] if includes.is_a?(Hash)
|
85
|
+
attribute_names = @attribute_map.serializable_attribute_names(Array(includes))
|
86
|
+
|
87
|
+
if @options[:only]
|
88
|
+
@options.delete(:except)
|
89
|
+
attribute_names & Array(@options[:only])
|
90
|
+
else
|
91
|
+
@options[:except] = Array(@options[:except])
|
92
|
+
attribute_names - @options[:except]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns the list of mapped association names that are to be output
|
97
|
+
# by applying the serializer's +:include+ to the attribute map.
|
98
|
+
def serializable_association_names(includes)
|
99
|
+
assoc_includes = includes
|
100
|
+
if assoc_includes.is_a?(Array)
|
101
|
+
if (h = includes.detect {|i| i.is_a?(Hash)})
|
102
|
+
assoc_includes = h.dup
|
103
|
+
includes.each { |i| assoc_includes[i] = {} unless i.is_a?(Hash) }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
include_has_options = assoc_includes.is_a?(Hash)
|
108
|
+
include_associations = include_has_options ? assoc_includes.keys : Array(assoc_includes)
|
109
|
+
associations = @attribute_map.serializable_association_names(include_associations)
|
110
|
+
|
111
|
+
if @options[:only]
|
112
|
+
@options.delete(:except)
|
113
|
+
associations = associations & Array(@options[:only])
|
114
|
+
else
|
115
|
+
@options[:except] = Array(@options[:except])
|
116
|
+
associations = associations - @options[:except]
|
117
|
+
end
|
118
|
+
|
119
|
+
[assoc_includes, associations]
|
120
|
+
end
|
121
|
+
|
122
|
+
# Helper for serializing nested models
|
123
|
+
def add_includes(&block)
|
124
|
+
includes = @options.delete(:include)
|
125
|
+
assoc_includes, associations = serializable_association_names(includes)
|
126
|
+
|
127
|
+
for association in associations
|
128
|
+
model_assoc = @attribute_map.model_association(association)
|
129
|
+
|
130
|
+
records = case reflection(model_assoc).macro
|
131
|
+
when :has_many, :has_and_belongs_to_many
|
132
|
+
@record.send(model_assoc).to_a
|
133
|
+
when :has_one, :belongs_to
|
134
|
+
@record.send(model_assoc)
|
135
|
+
end
|
136
|
+
|
137
|
+
yield(association, records, assoc_includes.is_a?(Hash) ? assoc_includes[association] : {}) if records
|
138
|
+
end
|
139
|
+
|
140
|
+
@options[:include] = includes if includes
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns an association's attribute map - argument is external name
|
144
|
+
def association_attribute_map(association)
|
145
|
+
@attribute_map.association_attribute_map(association)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns the attribute map for the specified record - ensures it
|
149
|
+
# is resolved and valid.
|
150
|
+
def attribute_map_for_record(record)
|
151
|
+
@attribute_map.validate(record.attribute_map(@attribute_map.name), record.class.name)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Gets association reflection
|
155
|
+
def reflection(model_assoc)
|
156
|
+
klass = @record.class
|
157
|
+
reflection = klass.reflect_on_association(model_assoc)
|
158
|
+
reflection || (klass.cti_base_class.reflect_on_association(model_assoc) if klass.is_cti_subclass?)
|
159
|
+
end
|
160
|
+
|
161
|
+
SERIALIZER_CLASS_OPTIONS = [:include, :only, :except, :context]
|
162
|
+
|
163
|
+
def remove_serializer_class_options(options)
|
164
|
+
options.reject {|k,v| SERIALIZER_CLASS_OPTIONS.include?(k)}
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Delineate
|
2
|
+
module AttributeMap
|
3
|
+
|
4
|
+
# AttributeMap serializer that handles XML as the external data format.
|
5
|
+
class XmlSerializer < MapSerializer
|
6
|
+
|
7
|
+
# Returns the record's mapped attributes as XML. The specified options
|
8
|
+
# are passed to the XML builder. Some typical options are:
|
9
|
+
#
|
10
|
+
# :root
|
11
|
+
# :dasherize
|
12
|
+
# :skip_types
|
13
|
+
# :skip_instruct
|
14
|
+
# :indent
|
15
|
+
#
|
16
|
+
def serialize(options = {})
|
17
|
+
hash = super()
|
18
|
+
|
19
|
+
if options[:root] == true
|
20
|
+
root_option = {:root => @record.class.model_name.element}
|
21
|
+
elsif options[:root]
|
22
|
+
root_option = {:root => options[:root]}
|
23
|
+
else
|
24
|
+
root_option = {}
|
25
|
+
end
|
26
|
+
opts = remove_serializer_class_options(options).merge(root_option)
|
27
|
+
|
28
|
+
hash.to_xml(opts)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Takes a record's attributes represented in XML, and returns a hash
|
32
|
+
# suitable for direct assignment to the record's collection of attributes.
|
33
|
+
# For example:
|
34
|
+
#
|
35
|
+
# s = Delineate::AttributeMap::XmlSerializer.new(record, :api)
|
36
|
+
# record.attributes = s.serialize_in(xml_string)
|
37
|
+
#
|
38
|
+
def serialize_in(xml_string, options = {})
|
39
|
+
super(Hash.from_xml(xml_string), options)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'delineate/attribute_map/attribute_map'
|
2
|
+
require 'delineate/attribute_map/map_serializer'
|
3
|
+
require 'class_inheritable_attributes'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
|
7
|
+
# Extend the ActiveRecord::Base class to include methods for defining and
|
8
|
+
# reading/writing the model API attributes.
|
9
|
+
class Base
|
10
|
+
|
11
|
+
# Collection of declared attribute maps for the model class
|
12
|
+
class_inheritable_accessor :attribute_maps
|
13
|
+
self.attribute_maps = {}
|
14
|
+
|
15
|
+
# The map_attributes method lets an ActiveRecord model class define a set
|
16
|
+
# of attributes that are to be exposed through the model's public interface.
|
17
|
+
# See the AttributeMap documentation for more information.
|
18
|
+
#
|
19
|
+
# The map_name parameter names the attribute map, and must be unique within
|
20
|
+
# a model class.
|
21
|
+
#
|
22
|
+
# class Account < ActiveRecord::Base
|
23
|
+
# map_attributes :api do
|
24
|
+
# .
|
25
|
+
# .
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# map_attributes :csv_export do
|
29
|
+
# .
|
30
|
+
# .
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# ActiveRecord STI subclasses inherit the attribute map of the same name
|
35
|
+
# from their superclass. If you want to include additional subclass attributes,
|
36
|
+
# just invoke map_attributes in the subclass and define the extra attributes
|
37
|
+
# and associations. If the subclass wants to completely override/replace
|
38
|
+
# the superclass map, do:
|
39
|
+
#
|
40
|
+
# map_attributes :api, :override => :replace do
|
41
|
+
# .
|
42
|
+
# .
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# To access (read) a model instance's attributes via an attribute map,
|
46
|
+
# you invoke a method on the instance named <map-name>_attributes. For
|
47
|
+
# example:
|
48
|
+
#
|
49
|
+
# attrs = post.api_attributes
|
50
|
+
#
|
51
|
+
# retrieves the attributes as specified in the Post model attribute
|
52
|
+
# map named :api. A hash of the API attributes is returned.
|
53
|
+
#
|
54
|
+
# An optional +options+ parameter lets you include attributes and
|
55
|
+
# associations that are defined as :optional in the attribute map.
|
56
|
+
# Following are some examples:
|
57
|
+
#
|
58
|
+
# post.api_attributes(:include => :author)
|
59
|
+
# post.api_attributes(:include => [:author, :comments])
|
60
|
+
#
|
61
|
+
# Include the balance attribute and also the description attribute in the
|
62
|
+
# :type association:
|
63
|
+
#
|
64
|
+
# account.api_attributes(:include => [:balance, {:type => :description}])
|
65
|
+
#
|
66
|
+
# Another exmpale: in the :type association, include the optional name attribute,
|
67
|
+
# the desc attribute of the category association, and all the mapped attributes of
|
68
|
+
# the :type association named :assoc2.
|
69
|
+
#
|
70
|
+
# account.api_attributes(:include => {:type => [:name, {:category => :desc}, :assoc2]})
|
71
|
+
#
|
72
|
+
# Other include forms:
|
73
|
+
# :include => :attr1
|
74
|
+
# :include => :assoc1
|
75
|
+
# :include => [:attr1, :attr2, :assoc1]
|
76
|
+
# :include => {:assoc1 => {}, :assoc2 => {:include => [:attr1, :attr2]}}
|
77
|
+
# :include => [:attr1, :attr2, {:assoc1 => {:include => :assoc2}}, :assoc3]
|
78
|
+
#
|
79
|
+
# In addition to the :include option, you can specify:
|
80
|
+
#
|
81
|
+
# :only Restricts the attributes and associations to only those specified.
|
82
|
+
# :except Processes attributes and associations except those specified.
|
83
|
+
#
|
84
|
+
# To update/set a model instance's attributes via an attribute map,
|
85
|
+
# you invoke a setter method on the instance named <map_name>_attributes=.
|
86
|
+
# For example:
|
87
|
+
#
|
88
|
+
# post.api_attributes = attr_hash
|
89
|
+
#
|
90
|
+
# The input hash contains name/value pairs, including those for nested
|
91
|
+
# models as defined as writeable in the attribute map. The input attribute
|
92
|
+
# values are mapped to the appropriate model attributes and associations.
|
93
|
+
#
|
94
|
+
# NOTE: Maps should pretty much be the last thing defined at the class level, but
|
95
|
+
# especially after the model class's associations and accepts_nested_attributes_for.
|
96
|
+
#
|
97
|
+
def self.map_attributes(map_name, options = {}, &blk)
|
98
|
+
map = Delineate::AttributeMap::AttributeMap.new(self.name, map_name, options)
|
99
|
+
|
100
|
+
# If this is a CTI subclass, init this map with its base class attributes and associations
|
101
|
+
if respond_to?(:is_cti_subclass) and is_cti_subclass? and options[:override] != :replace
|
102
|
+
base_class_map = cti_base_class.attribute_map(map_name)
|
103
|
+
raise "Base class for CTI subclass #{self.name} must specify attribute map #{map_name}" if base_class_map.nil?
|
104
|
+
|
105
|
+
base_class_map.attributes.each { |attr, opts| map.attribute(attr, opts.dup) }
|
106
|
+
base_class_map.associations.each do |name, assoc|
|
107
|
+
map.association(name, assoc[:options].merge({:attr_map => assoc[:attr_map].try(:dup)})) unless assoc[:klass_name] == self.name
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Parse the map specification DSL
|
112
|
+
map.instance_eval(&blk)
|
113
|
+
|
114
|
+
define_attribute_map_methods(map_name) # define map accessor methods
|
115
|
+
attribute_maps[map_name] = map
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.attribute_map(map_name)
|
119
|
+
attribute_maps.try(:fetch, map_name, nil)
|
120
|
+
end
|
121
|
+
|
122
|
+
def attribute_map(map_name)
|
123
|
+
self.class.attribute_maps[map_name]
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns the attributes as specified in the attribut map. The +format+ paramater
|
127
|
+
# can be one of the following: :hash, :json, :xml, :csv.
|
128
|
+
#
|
129
|
+
# The supported options hash keys are:
|
130
|
+
#
|
131
|
+
# :include Specifies which optional attributes and associations to output.
|
132
|
+
# :only Restricts the attributes and associations to only those specified.
|
133
|
+
# :except Processes attributes and associations except those specified.
|
134
|
+
# :context If this option is specified, then attribute readers and writers
|
135
|
+
# defined as symbols will be executed as instance methods on the
|
136
|
+
# specified context object.
|
137
|
+
#
|
138
|
+
def mapped_attributes(map_name, format = :hash, options = {})
|
139
|
+
map = validate_parameters(map_name, format)
|
140
|
+
@serializer_context = options[:context]
|
141
|
+
|
142
|
+
serializer_class(format).new(self, map, options).serialize(options)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Sets the model object's attributes from the input hash. The hash contains
|
146
|
+
# name/value pairs, including those for nested models as defined as writeable
|
147
|
+
# in the attribute map. The input attribute names are mapped to the appropriate
|
148
|
+
# model attributes and associations.
|
149
|
+
#
|
150
|
+
def mapped_attributes=(map_name, attrs, format = :hash, options = {})
|
151
|
+
map = validate_parameters(map_name, format)
|
152
|
+
@serializer_context = options[:context]
|
153
|
+
|
154
|
+
self.attributes = serializer_class(format).new(self, map).serialize_in(attrs, options)
|
155
|
+
end
|
156
|
+
alias_method :set_mapped_attributes, :mapped_attributes=
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
# Defines the attribute map accessor methods:
|
161
|
+
#
|
162
|
+
# def api_attributes([format = :hash,] options = {}) # returns the mapped attributes as a hash
|
163
|
+
# def api_attributes=(attr_hash, options={}) # sets model attributes via the map
|
164
|
+
#
|
165
|
+
def self.define_attribute_map_methods(map_name)
|
166
|
+
class_eval do
|
167
|
+
define_method("#{map_name}_attributes") do |*args|
|
168
|
+
format = args.first && args.first.is_a?(Symbol) ? args.first : :hash
|
169
|
+
options = args.last.is_a?(Hash) ? args.last : {}
|
170
|
+
mapped_attributes(map_name, format, options)
|
171
|
+
end
|
172
|
+
|
173
|
+
define_method("#{map_name}_attributes=") do |attr_hash|
|
174
|
+
set_mapped_attributes(map_name, attr_hash, :hash)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def serializer_class(format)
|
180
|
+
if format == :hash
|
181
|
+
Delineate::AttributeMap::MapSerializer
|
182
|
+
else
|
183
|
+
"Delineate::AttributeMap::#{format.to_s.camelize}Serializer".constantize
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def validate_parameters(map_name, format)
|
188
|
+
map = map_name
|
189
|
+
if map_name.is_a? Symbol
|
190
|
+
map = attribute_map(map_name)
|
191
|
+
raise ArgumentError, "Missing attribute map :#{map_name} for class #{self.class.name}" if map.nil?
|
192
|
+
end
|
193
|
+
|
194
|
+
raise ArgumentError, "The map parameter :#{map_name} for class #{self.class.name} is invalid" if !map.is_a?(Delineate::AttributeMap::AttributeMap)
|
195
|
+
raise ArgumentError, 'Invalid format parameter' unless [:hash, :csv, :xml, :json].include?(format)
|
196
|
+
map
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|