delineate 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|