brainstem 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/Gemfile.lock +51 -0
- data/Guardfile +8 -0
- data/LICENSE +22 -0
- data/README.md +181 -0
- data/Rakefile +6 -0
- data/brainstem.gemspec +28 -0
- data/lib/brainstem.rb +63 -0
- data/lib/brainstem/association_field.rb +35 -0
- data/lib/brainstem/controller_methods.rb +44 -0
- data/lib/brainstem/engine.rb +4 -0
- data/lib/brainstem/presenter.rb +210 -0
- data/lib/brainstem/presenter_collection.rb +279 -0
- data/lib/brainstem/time_classes.rb +14 -0
- data/lib/brainstem/version.rb +3 -0
- data/spec/brainstem/controller_methods_spec.rb +68 -0
- data/spec/brainstem/presenter_collection_spec.rb +486 -0
- data/spec/brainstem/presenter_spec.rb +252 -0
- data/spec/brainstem_spec.rb +25 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/spec_helpers/cleanup.rb +23 -0
- data/spec/spec_helpers/db.rb +79 -0
- data/spec/spec_helpers/presenters.rb +39 -0
- metadata +201 -0
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'brainstem/association_field'
|
3
|
+
require 'brainstem/time_classes'
|
4
|
+
|
5
|
+
module Brainstem
|
6
|
+
# @abstract Subclass and override {#present} to implement a presenter.
|
7
|
+
class Presenter
|
8
|
+
|
9
|
+
# Class methods
|
10
|
+
|
11
|
+
# Accepts a list of classes this presenter knows how to present.
|
12
|
+
# @param [String, [String]] klasses Any number of names of classes this presenter presents.
|
13
|
+
def self.presents(*klasses)
|
14
|
+
Brainstem.add_presenter_class(self, *klasses)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @overload default_sort_order(sort_string)
|
18
|
+
# Sets a default sort order.
|
19
|
+
# @param [String] sort_string The sort order to apply by default while presenting. The string must contain the name of a sort order that has explicitly been declared using {sort_order}. The string may end in +:asc+ or +:desc+ to indicate the default order's direction.
|
20
|
+
# @return [String] The new default sort order.
|
21
|
+
# @overload default_sort_order
|
22
|
+
# @return [String] The default sort order, or nil if one is not set.
|
23
|
+
def self.default_sort_order(sort_string = nil)
|
24
|
+
if sort_string
|
25
|
+
@default_sort_order = sort_string
|
26
|
+
else
|
27
|
+
@default_sort_order
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# @overload sort_order(name, order)
|
32
|
+
# @param [Symbol] name The name of the sort order.
|
33
|
+
# @param [String] order The SQL string to use to sort the presented data.
|
34
|
+
# @overload sort_order(name, &block)
|
35
|
+
# @yieldparam scope [ActiveRecord::Relation] The scope representing the data being presented.
|
36
|
+
# @yieldreturn [ActiveRecord::Relation] A new scope that adds ordering requirements to the scope that was yielded.
|
37
|
+
# Create a named sort order, either containing a string to use as ORDER in a query, or with a block that adds an order Arel predicate to a scope.
|
38
|
+
# @raise [ArgumentError] if neither an order string or block is given.
|
39
|
+
def self.sort_order(name, order = nil, &block)
|
40
|
+
raise ArgumentError, "A sort order must be given" unless block_given? || order
|
41
|
+
@sort_orders ||= {}
|
42
|
+
@sort_orders[name] = (block_given? ? block : order)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Hash] All defined sort orders, keyed by their name.
|
46
|
+
def self.sort_orders
|
47
|
+
@sort_orders
|
48
|
+
end
|
49
|
+
|
50
|
+
# @overload filter(name, options = {})
|
51
|
+
# @param [Symbol] name The name of the scope that may be applied as a filter.
|
52
|
+
# @option options [Object] :default If set, causes this filter to be applied to every request. If the filter accepts parameters, the value given here will be passed to the filter when it is applied.
|
53
|
+
# @overload filter(name, options = {}, &block)
|
54
|
+
# @param [Symbol] name The filter can be requested using this name.
|
55
|
+
# @yieldparam scope [ActiveRecord::Relation] The scope that the filter should use as a base.
|
56
|
+
# @yieldparam arg [Object] The argument passed when the filter was requested.
|
57
|
+
# @yieldreturn [ActiveRecord::Relation] A new scope that filters the scope that was yielded.
|
58
|
+
def self.filter(name, options = {}, &block)
|
59
|
+
@filters ||= {}
|
60
|
+
@filters[name] = [options, (block_given? ? block : nil)]
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Hash] All defined filters, keyed by their name.
|
64
|
+
def self.filters
|
65
|
+
@filters
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.search(&block)
|
69
|
+
@search_block = block
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.search_block
|
73
|
+
@search_block
|
74
|
+
end
|
75
|
+
|
76
|
+
# Declares a helper module whose methods will be available in instances of the presenter class and available inside sort and filter blocks.
|
77
|
+
# @param [Module] mod A module whose methods will be made available to filter and sort blocks, as well as inside the {#present} method.
|
78
|
+
# @return [self]
|
79
|
+
def self.helper(mod)
|
80
|
+
include mod
|
81
|
+
extend mod
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# Instance methods
|
86
|
+
|
87
|
+
# @raise [RuntimeError] if this method has not been overridden in the presenter subclass.
|
88
|
+
def present(model)
|
89
|
+
raise "Please override #present(model) in your subclass of Brainstem::Presenter"
|
90
|
+
end
|
91
|
+
|
92
|
+
# @api private
|
93
|
+
# Calls {#post_process} on the output from {#present}.
|
94
|
+
# @return (see #post_process)
|
95
|
+
def present_and_post_process(model, associations = [])
|
96
|
+
post_process(present(model), model, associations)
|
97
|
+
end
|
98
|
+
|
99
|
+
# @api private
|
100
|
+
# Loads associations and converts dates to epoch strings.
|
101
|
+
# @return [Hash] The hash representing the models and associations, ready to be converted to JSON.
|
102
|
+
def post_process(struct, model, associations = [])
|
103
|
+
add_id(model, struct)
|
104
|
+
load_associations!(model, struct, associations)
|
105
|
+
datetimes_to_json(struct)
|
106
|
+
end
|
107
|
+
|
108
|
+
# @api private
|
109
|
+
# Adds :id as a string from the given model.
|
110
|
+
def add_id(model, struct)
|
111
|
+
if model.class.respond_to?(:primary_key)
|
112
|
+
struct[:id] = model[model.class.primary_key].to_s
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# @api private
|
117
|
+
# Calls {#custom_preload}, and then {#present} and {#post_process}, for each model.
|
118
|
+
def group_present(models, associations = [])
|
119
|
+
custom_preload models, associations
|
120
|
+
|
121
|
+
models.map do |model|
|
122
|
+
present_and_post_process model, associations
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Subclasses can define this if they wish. This method will be called before {#present}.
|
127
|
+
def custom_preload(models, associations = [])
|
128
|
+
end
|
129
|
+
|
130
|
+
# @api private
|
131
|
+
# Recurses through any nested Hash/Array data structure, converting dates and times to JSON standard values.
|
132
|
+
def datetimes_to_json(struct)
|
133
|
+
case struct
|
134
|
+
when Array
|
135
|
+
struct.map { |value| datetimes_to_json(value) }
|
136
|
+
when Hash
|
137
|
+
processed = {}
|
138
|
+
struct.each { |k,v| processed[k] = datetimes_to_json(v) }
|
139
|
+
processed
|
140
|
+
when Date
|
141
|
+
struct.strftime('%F')
|
142
|
+
when *TIME_CLASSES # Time, ActiveSupport::TimeWithZone
|
143
|
+
struct.iso8601
|
144
|
+
else
|
145
|
+
struct
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# @api private
|
150
|
+
# Makes sure that associations are loaded and converted into ids.
|
151
|
+
def load_associations!(model, struct, associations)
|
152
|
+
struct.to_a.each do |key, value|
|
153
|
+
if value.is_a?(AssociationField)
|
154
|
+
struct.delete key
|
155
|
+
id_attr = value.method_name ? "#{value.method_name}_id" : nil
|
156
|
+
|
157
|
+
if id_attr && model.class.columns_hash.has_key?(id_attr)
|
158
|
+
struct["#{key}_id".to_sym] = to_s_except_nil(model.send(id_attr))
|
159
|
+
reflection = value.method_name && model.reflections[value.method_name.to_sym]
|
160
|
+
if reflection && reflection.options[:polymorphic]
|
161
|
+
struct["#{key.to_s.singularize}_type".to_sym] = model.send("#{value.method_name}_type")
|
162
|
+
end
|
163
|
+
elsif associations.include?(key)
|
164
|
+
result = value.call(model)
|
165
|
+
if result.is_a?(Array)
|
166
|
+
struct["#{key.to_s.singularize}_ids".to_sym] = result.map {|a| to_s_except_nil(a.is_a?(ActiveRecord::Base) ? a.id : a) }
|
167
|
+
else
|
168
|
+
if result.is_a?(ActiveRecord::Base)
|
169
|
+
struct["#{key.to_s.singularize}_id".to_sym] = to_s_except_nil(result.id)
|
170
|
+
else
|
171
|
+
struct["#{key.to_s.singularize}_id".to_sym] = to_s_except_nil(result)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# @!attribute [r] default_sort_order
|
180
|
+
# The default sort order set on this presenter's class.
|
181
|
+
def default_sort_order
|
182
|
+
self.class.default_sort_order
|
183
|
+
end
|
184
|
+
|
185
|
+
# @!attribute [r] sort_orders
|
186
|
+
# The sort orders that were declared in the definition of this presenter.
|
187
|
+
def sort_orders
|
188
|
+
self.class.sort_orders
|
189
|
+
end
|
190
|
+
|
191
|
+
# @!attribute [r] filters
|
192
|
+
# The filters that were declared in the definition of this presenter.
|
193
|
+
def filters
|
194
|
+
self.class.filters
|
195
|
+
end
|
196
|
+
|
197
|
+
def search_block
|
198
|
+
self.class.search_block
|
199
|
+
end
|
200
|
+
|
201
|
+
# An association on the object being presented that should be included in the presented data.
|
202
|
+
def association(method_name = nil, options = {}, &block)
|
203
|
+
AssociationField.new method_name, options, &block
|
204
|
+
end
|
205
|
+
|
206
|
+
def to_s_except_nil(thing)
|
207
|
+
thing.nil? ? nil : thing.to_s
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,279 @@
|
|
1
|
+
require 'brainstem/association_field'
|
2
|
+
|
3
|
+
module Brainstem
|
4
|
+
class PresenterCollection
|
5
|
+
|
6
|
+
# @!attribute default_max_per_page
|
7
|
+
# @return [Integer] The maximum number of objects that can be requested in a single presented hash.
|
8
|
+
attr_accessor :default_max_per_page
|
9
|
+
|
10
|
+
# @!attribute default_per_page
|
11
|
+
# @return [Integer] The default number of objects that will be returned in the presented hash.
|
12
|
+
attr_accessor :default_per_page
|
13
|
+
|
14
|
+
# @!visibility private
|
15
|
+
def initialize
|
16
|
+
@default_per_page = 20
|
17
|
+
@default_max_per_page = 200
|
18
|
+
end
|
19
|
+
|
20
|
+
# The main presentation method, converting a model name and an optional scope into a hash structure, ready to be converted into JSON.
|
21
|
+
# @param [Class, String] name The class of the objects to be presented.
|
22
|
+
# @param [Hash] options The options that will be applied as the objects are converted.
|
23
|
+
# @option options [Hash] :params The +params+ hash included in a request for the presented object.
|
24
|
+
# @option options [ActiveRecord::Base] :model The model that is being presented (if different from +name+).
|
25
|
+
# @option options [String] :as The top-level key the presented objects will be assigned to (if different from +name.tableize+)
|
26
|
+
# @option options [Integer] :max_per_page The maximum number of items that can be requested by <code>params[:per_page]</code>.
|
27
|
+
# @option options [Integer] :per_page The number of items that will be returned if <code>params[:per_page]</code> is not set.
|
28
|
+
# @option options [Boolean] :apply_default_filters Determine if Presenter's filter defaults should be applied. On by default.
|
29
|
+
# @yield Must return a scope on the model +name+, which will then be presented.
|
30
|
+
# @return [Hash] A hash of arrays of hashes. Top-level hash keys are pluralized model names, with values of arrays containing one hash per object that was found by the given given options.
|
31
|
+
def presenting(name, options = {}, &block)
|
32
|
+
options[:params] ||= {}
|
33
|
+
presented_class = (options[:model] || name)
|
34
|
+
presented_class = presented_class.classify.constantize if presented_class.is_a?(String)
|
35
|
+
scope = presented_class.instance_eval(&block)
|
36
|
+
|
37
|
+
# grab the presenter that knows about filters and sorting etc.
|
38
|
+
options[:presenter] = for!(presented_class)
|
39
|
+
|
40
|
+
# table name will be used to query the database for the filtered data
|
41
|
+
options[:table_name] = presented_class.table_name
|
42
|
+
|
43
|
+
# key these models will use in the struct that is output
|
44
|
+
options[:as] = (options[:as] || name.to_s.tableize).to_sym
|
45
|
+
|
46
|
+
# Filter
|
47
|
+
scope = run_filters scope, options
|
48
|
+
|
49
|
+
# Search
|
50
|
+
scope = run_search scope, options
|
51
|
+
|
52
|
+
if options[:params][:only].present?
|
53
|
+
# Handle Only
|
54
|
+
scope, count = handle_only(scope, options[:params][:only])
|
55
|
+
else
|
56
|
+
# Paginate
|
57
|
+
scope, count = paginate scope, options
|
58
|
+
end
|
59
|
+
|
60
|
+
# Ordering
|
61
|
+
scope = handle_ordering scope, options
|
62
|
+
|
63
|
+
# Load Includes
|
64
|
+
records = scope.to_a
|
65
|
+
model = records.first
|
66
|
+
|
67
|
+
allowed_includes = calculate_allowed_includes options[:presenter], presented_class, records
|
68
|
+
includes_hash = filter_includes options[:params][:include], allowed_includes
|
69
|
+
models = perform_preloading records, includes_hash
|
70
|
+
primary_models, associated_models = gather_associations(models, includes_hash)
|
71
|
+
struct = { :count => count, options[:as] => [], :results => [] }
|
72
|
+
|
73
|
+
associated_models.each do |json_name, models|
|
74
|
+
models.flatten!
|
75
|
+
models.uniq!
|
76
|
+
|
77
|
+
if models.length > 0
|
78
|
+
presenter = for!(models.first.class)
|
79
|
+
assoc = includes_hash.to_a.find { |k, v| v[:json_name] == json_name }
|
80
|
+
struct[json_name] = presenter.group_present(models, [])
|
81
|
+
else
|
82
|
+
struct[json_name] = []
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
if primary_models.length > 0
|
87
|
+
presented_primary_models = options[:presenter].group_present(models, includes_hash.keys)
|
88
|
+
struct[options[:as]] += presented_primary_models
|
89
|
+
struct[:results] = presented_primary_models.map { |model| { :key => options[:as].to_s, :id => model[:id] } }
|
90
|
+
end
|
91
|
+
|
92
|
+
rewrite_keys_as_objects!(struct)
|
93
|
+
|
94
|
+
struct
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [Hash] The presenters this collection knows about, keyed on the names of the classes that can be presented.
|
98
|
+
def presenters
|
99
|
+
@presenters ||= {}
|
100
|
+
end
|
101
|
+
|
102
|
+
# @param [String, Class] presenter_class The presenter class that knows how to present all of the classes given in +klasses+.
|
103
|
+
# @param [*Class] klasses One or more classes that can be presented by +presenter_class+.
|
104
|
+
def add_presenter_class(presenter_class, *klasses)
|
105
|
+
klasses.each do |klass|
|
106
|
+
presenters[klass.to_s] = presenter_class.new
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# @return [Brainstem::Presenter, nil] The presenter that knows how to present the class +klass+, or +nil+ if there isn't one.
|
111
|
+
def for(klass)
|
112
|
+
presenters[klass.to_s]
|
113
|
+
end
|
114
|
+
|
115
|
+
# @return [Brainstem::Presenter] The presenter that knows how to present the class +klass+.
|
116
|
+
# @raise [ArgumentError] if there is no known presenter for +klass+.
|
117
|
+
def for!(klass)
|
118
|
+
self.for(klass) || raise(ArgumentError, "Unable to find a presenter for class #{klass}")
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def paginate(scope, options)
|
124
|
+
max_per_page = (options[:max_per_page] || default_max_per_page).to_i
|
125
|
+
per_page = (options[:params][:per_page] || options[:per_page] || default_per_page).to_i
|
126
|
+
per_page = max_per_page if per_page > max_per_page
|
127
|
+
per_page = (options[:per_page] || default_per_page).to_i if per_page < 1
|
128
|
+
|
129
|
+
page = (options[:params][:page] || 1).to_i
|
130
|
+
page = 1 if page < 1
|
131
|
+
|
132
|
+
[scope.limit(per_page).offset(per_page * (page - 1)).uniq, scope.select("distinct `#{options[:table_name]}`.id").count] # as of Rails 3.2.5, uniq.count generates the wrong SQL.
|
133
|
+
end
|
134
|
+
|
135
|
+
# Gather allowed includes by inspecting the presented hash. For now, this requires that a new instance of the
|
136
|
+
# presented class always be presentable.
|
137
|
+
def calculate_allowed_includes(presenter, presented_class, records)
|
138
|
+
allowed_includes = {}
|
139
|
+
model = records.first || presented_class.new
|
140
|
+
presenter.present(model).each do |k, v|
|
141
|
+
next unless v.is_a?(AssociationField)
|
142
|
+
|
143
|
+
if v.json_name
|
144
|
+
v.json_name = v.json_name.tableize
|
145
|
+
else
|
146
|
+
association = model.class.reflections[v.method_name]
|
147
|
+
if !association.options[:polymorphic]
|
148
|
+
v.json_name = association && association.table_name
|
149
|
+
if v.json_name.nil?
|
150
|
+
raise ":json_name is a required option for method-based associations (#{presented_class}##{v.method_name})"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
allowed_includes[k.to_s] = v
|
155
|
+
end
|
156
|
+
allowed_includes
|
157
|
+
end
|
158
|
+
|
159
|
+
def filter_includes(user_includes, allowed_includes)
|
160
|
+
filtered_includes = {}
|
161
|
+
(user_includes || "").split(',').each do |k|
|
162
|
+
allowed = allowed_includes[k]
|
163
|
+
if allowed
|
164
|
+
filtered_includes[k.to_sym] = {
|
165
|
+
:association => allowed.method_name.to_sym,
|
166
|
+
:json_name => allowed.json_name.try(:to_sym)
|
167
|
+
}
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
filtered_includes
|
172
|
+
end
|
173
|
+
|
174
|
+
def handle_only(scope, only)
|
175
|
+
ids = (only || "").split(",").select {|id| id =~ /\A\d+\Z/}.uniq
|
176
|
+
[scope.where(:id => ids), scope.where(:id => ids).count]
|
177
|
+
end
|
178
|
+
|
179
|
+
def run_filters(scope, options)
|
180
|
+
run_defaults = options.has_key?(:apply_default_filters) ? options[:apply_default_filters] : true
|
181
|
+
|
182
|
+
(options[:presenter].filters || {}).each do |filter_name, filter|
|
183
|
+
requested = options[:params][filter_name]
|
184
|
+
requested = requested.present? ? requested.to_s : nil
|
185
|
+
requested = requested == "true" ? true : (requested == "false" ? false : requested)
|
186
|
+
|
187
|
+
filter_options, filter_lambda = filter
|
188
|
+
args = run_defaults ? (requested || filter_options[:default]) : requested
|
189
|
+
next if args.nil?
|
190
|
+
|
191
|
+
if filter_lambda
|
192
|
+
scope = filter_lambda.call(scope, *args)
|
193
|
+
else
|
194
|
+
scope = scope.send(filter_name, *args)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
scope
|
199
|
+
end
|
200
|
+
|
201
|
+
def run_search(scope, options)
|
202
|
+
return scope unless options[:params][:search] && options[:presenter].search_block.present?
|
203
|
+
|
204
|
+
result_ids = options[:presenter].search_block.call(options[:params][:search])
|
205
|
+
scope.where(:id => result_ids )
|
206
|
+
end
|
207
|
+
|
208
|
+
def handle_ordering(scope, options)
|
209
|
+
default_column, default_direction = (options[:presenter].default_sort_order || "updated_at:desc").split(":")
|
210
|
+
sort_name, direction = (options[:params][:order] || "").split(":")
|
211
|
+
sort_orders = (options[:presenter].sort_orders || {})
|
212
|
+
|
213
|
+
if sort_name.present? && sort_orders[sort_name.to_sym]
|
214
|
+
order = sort_orders[sort_name.to_sym]
|
215
|
+
else
|
216
|
+
order = sort_orders[default_column.to_sym]
|
217
|
+
direction = default_direction
|
218
|
+
end
|
219
|
+
|
220
|
+
case order
|
221
|
+
when Proc
|
222
|
+
order.call(scope, direction == "desc" ? "desc" : "asc")
|
223
|
+
when nil
|
224
|
+
scope
|
225
|
+
else
|
226
|
+
scope.order(order.to_s + " " + (direction == "desc" ? "desc" : "asc"))
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def perform_preloading(records, includes_hash)
|
231
|
+
records.tap do |models|
|
232
|
+
association_names_to_preload = includes_hash.values.map {|i| i[:association] }
|
233
|
+
if models.first
|
234
|
+
reflections = models.first.reflections
|
235
|
+
association_names_to_preload.reject! { |association| !reflections.has_key?(association) }
|
236
|
+
end
|
237
|
+
if association_names_to_preload.any?
|
238
|
+
ActiveRecord::Associations::Preloader.new(models, association_names_to_preload).run
|
239
|
+
Brainstem.logger.info "Eager loaded #{association_names_to_preload.join(", ")}."
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def gather_associations(models, includes_hash)
|
245
|
+
record_hash = {}
|
246
|
+
primary_models = []
|
247
|
+
|
248
|
+
includes_hash.each do |include, include_data|
|
249
|
+
record_hash[include_data[:json_name]] ||= [] if include_data[:json_name]
|
250
|
+
end
|
251
|
+
|
252
|
+
models.each do |model|
|
253
|
+
primary_models << model
|
254
|
+
|
255
|
+
includes_hash.each do |include, include_data|
|
256
|
+
models = Array(model.send(include_data[:association]))
|
257
|
+
if include_data[:json_name]
|
258
|
+
record_hash[include_data[:json_name]] += models
|
259
|
+
else
|
260
|
+
# polymorphic associations' tables must be figured out now
|
261
|
+
models.each do |record|
|
262
|
+
json_name = record.class.table_name.to_sym
|
263
|
+
record_hash[json_name] ||= []
|
264
|
+
record_hash[json_name] << record
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
[primary_models, record_hash]
|
271
|
+
end
|
272
|
+
|
273
|
+
def rewrite_keys_as_objects!(struct)
|
274
|
+
(struct.keys - [:count, :results]).each do |key|
|
275
|
+
struct[key] = struct[key].inject({}) {|memo, obj| memo[obj[:id] || obj["id"] || "unknown_id"] = obj; memo }
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|