praxis 0.22.pre.1 → 2.0.pre.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +5 -20
- data/CHANGELOG.md +328 -323
- data/lib/praxis.rb +13 -9
- data/lib/praxis/action_definition.rb +8 -10
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
- data/lib/praxis/api_definition.rb +27 -44
- data/lib/praxis/api_general_info.rb +2 -3
- data/lib/praxis/application.rb +15 -142
- data/lib/praxis/bootloader.rb +1 -2
- data/lib/praxis/bootloader_stages/environment.rb +13 -0
- data/lib/praxis/config.rb +1 -1
- data/lib/praxis/controller.rb +0 -2
- data/lib/praxis/dispatcher.rb +4 -6
- data/lib/praxis/docs/generator.rb +8 -18
- data/lib/praxis/docs/link_builder.rb +1 -1
- data/lib/praxis/error_handler.rb +5 -5
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
- data/lib/praxis/extensions/field_selection.rb +1 -12
- data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +28 -34
- data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +35 -39
- data/lib/praxis/extensions/rendering.rb +1 -1
- data/lib/praxis/file_group.rb +1 -1
- data/lib/praxis/handlers/xml.rb +1 -1
- data/lib/praxis/mapper/active_model_compat.rb +98 -0
- data/lib/praxis/mapper/resource.rb +242 -0
- data/lib/praxis/mapper/selector_generator.rb +154 -0
- data/lib/praxis/mapper/sequel_compat.rb +76 -0
- data/lib/praxis/media_type_identifier.rb +2 -1
- data/lib/praxis/middleware_app.rb +13 -15
- data/lib/praxis/multipart/part.rb +3 -5
- data/lib/praxis/notifications.rb +1 -1
- data/lib/praxis/plugins/mapper_plugin.rb +64 -0
- data/lib/praxis/request.rb +14 -7
- data/lib/praxis/request_stages/response.rb +2 -3
- data/lib/praxis/resource_definition.rb +15 -19
- data/lib/praxis/response.rb +6 -5
- data/lib/praxis/response_definition.rb +5 -7
- data/lib/praxis/response_template.rb +3 -4
- data/lib/praxis/responses/http.rb +36 -0
- data/lib/praxis/responses/internal_server_error.rb +12 -3
- data/lib/praxis/responses/multipart_ok.rb +11 -4
- data/lib/praxis/responses/validation_error.rb +10 -1
- data/lib/praxis/route.rb +1 -1
- data/lib/praxis/router.rb +3 -3
- data/lib/praxis/routing_config.rb +1 -1
- data/lib/praxis/tasks/api_docs.rb +2 -10
- data/lib/praxis/tasks/routes.rb +0 -1
- data/lib/praxis/trait.rb +1 -1
- data/lib/praxis/types/media_type_common.rb +2 -2
- data/lib/praxis/types/multipart.rb +1 -1
- data/lib/praxis/types/multipart_array.rb +2 -2
- data/lib/praxis/types/multipart_array/part_definition.rb +1 -1
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +11 -9
- data/spec/functional_spec.rb +0 -1
- data/spec/praxis/action_definition_spec.rb +16 -27
- data/spec/praxis/api_definition_spec.rb +8 -13
- data/spec/praxis/api_general_info_spec.rb +8 -3
- data/spec/praxis/application_spec.rb +8 -14
- data/spec/praxis/collection_spec.rb +3 -2
- data/spec/praxis/config_spec.rb +2 -2
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +106 -0
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +147 -0
- data/spec/praxis/extensions/field_selection/support/spec_resources_active_model.rb +130 -0
- data/spec/praxis/extensions/field_selection/support/spec_resources_sequel.rb +106 -0
- data/spec/praxis/handlers/xml_spec.rb +2 -2
- data/spec/praxis/mapper/resource_spec.rb +169 -0
- data/spec/praxis/mapper/selector_generator_spec.rb +325 -0
- data/spec/praxis/media_type_spec.rb +0 -10
- data/spec/praxis/middleware_app_spec.rb +16 -10
- data/spec/praxis/request_spec.rb +7 -17
- data/spec/praxis/request_stages/action_spec.rb +8 -1
- data/spec/praxis/request_stages/validate_spec.rb +1 -1
- data/spec/praxis/resource_definition_spec.rb +10 -12
- data/spec/praxis/response_definition_spec.rb +12 -26
- data/spec/praxis/response_spec.rb +6 -13
- data/spec/praxis/responses/internal_server_error_spec.rb +5 -2
- data/spec/praxis/router_spec.rb +5 -9
- data/spec/spec_app/app/controllers/instances.rb +1 -1
- data/spec/spec_app/config.ru +6 -1
- data/spec/spec_app/config/environment.rb +3 -21
- data/spec/spec_helper.rb +13 -17
- data/spec/support/be_deep_equal_matcher.rb +39 -0
- data/spec/support/spec_resources.rb +124 -0
- metadata +74 -53
- data/lib/praxis/extensions/attribute_filtering.rb +0 -28
- data/lib/praxis/extensions/attribute_filtering/query_builder.rb +0 -39
- data/lib/praxis/extensions/mapper_selectors.rb +0 -16
- data/lib/praxis/media_type_collection.rb +0 -127
- data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
- data/spec/praxis/media_type_collection_spec.rb +0 -157
- data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
- data/spec/spec_app/app/models/person.rb +0 -3
@@ -24,7 +24,7 @@ module Praxis
|
|
24
24
|
response.body = render(object, include_nil: include_nil)
|
25
25
|
response
|
26
26
|
rescue Praxis::Renderer::CircularRenderingError => e
|
27
|
-
Praxis::Application.
|
27
|
+
Praxis::Application.instance.validation_handler.handle!(
|
28
28
|
summary: "Circular Rendering Error when rendering response. " +
|
29
29
|
"Please especify a view to narrow the dependent fields, or narrow your field set.",
|
30
30
|
exception: e,
|
data/lib/praxis/file_group.rb
CHANGED
data/lib/praxis/handlers/xml.rb
CHANGED
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
require 'praxis/extensions/field_selection/active_record_query_selector'
|
6
|
+
|
7
|
+
module Praxis
|
8
|
+
module Mapper
|
9
|
+
module ActiveModelCompat
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
attr_accessor :_resource
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def _filter_query_builder_class
|
18
|
+
Praxis::Extensions::ActiveRecordFilterQueryBuilder
|
19
|
+
end
|
20
|
+
|
21
|
+
def _field_selector_query_builder_class
|
22
|
+
Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector
|
23
|
+
end
|
24
|
+
|
25
|
+
def _praxis_associations
|
26
|
+
orig = self.reflections.clone
|
27
|
+
|
28
|
+
orig.each_with_object({}) do |(k, v), hash|
|
29
|
+
# Assume an 'id' primary key if the system is initializing without AR connected
|
30
|
+
# (or without the tables created). This probably means that it's a rake task initializing or so...
|
31
|
+
pkey = \
|
32
|
+
if v.klass.connected? && v.klass.table_exists?
|
33
|
+
v.klass.primary_key
|
34
|
+
else
|
35
|
+
'id'
|
36
|
+
end
|
37
|
+
info = { model: v.klass, primary_key: pkey }
|
38
|
+
info[:type] = \
|
39
|
+
case v
|
40
|
+
when ActiveRecord::Reflection::BelongsToReflection
|
41
|
+
:many_to_one
|
42
|
+
when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasOneReflection
|
43
|
+
:one_to_many
|
44
|
+
when ActiveRecord::Reflection::ThroughReflection
|
45
|
+
:many_to_many
|
46
|
+
else
|
47
|
+
raise "Unknown association type: #{v.class.name} on #{v.klass.name} for #{v.name}"
|
48
|
+
end
|
49
|
+
# Call out any local (i.e., of this model) columns that participate in the association
|
50
|
+
info[:local_key_columns] = local_columns_used_for_the_association(info[:type], v)
|
51
|
+
info[:remote_key_columns] = remote_columns_used_for_the_association(info[:type], v)
|
52
|
+
|
53
|
+
if v.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
54
|
+
info[:through] = v.through_reflection.name # TODO: is this correct?
|
55
|
+
end
|
56
|
+
hash[k.to_sym] = info
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def local_columns_used_for_the_association(type, assoc_reflection)
|
62
|
+
case type
|
63
|
+
when :one_to_many
|
64
|
+
# The associated table will point to us by key (usually the PK, but not always)
|
65
|
+
[assoc_reflection.join_keys.foreign_key.to_sym]
|
66
|
+
when :many_to_one
|
67
|
+
# We have the FKs to the associated model
|
68
|
+
[assoc_reflection.join_keys.foreign_key.to_sym]
|
69
|
+
when :many_to_many
|
70
|
+
ref = resolve_closest_through_reflection(assoc_reflection)
|
71
|
+
# The associated middle table will point to us by key (usually the PK, but not always)
|
72
|
+
[ref.join_keys.foreign_key.to_sym] # The foreign key that the last through table points to
|
73
|
+
else
|
74
|
+
raise "association type #{type} not supported"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def remote_columns_used_for_the_association(type, assoc_reflection)
|
79
|
+
# It seems that since the reflection is the target of the association, using the join_keys.key
|
80
|
+
# will always get us the right column
|
81
|
+
case type
|
82
|
+
when :one_to_many, :many_to_one, :many_to_many
|
83
|
+
[assoc_reflection.join_keys.key.to_sym]
|
84
|
+
else
|
85
|
+
raise "association type #{type} not supported"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Keep following the association reflections as long as there are middle ones (i.e., through)
|
90
|
+
# until we come to the one next to the source
|
91
|
+
def resolve_closest_through_reflection(ref)
|
92
|
+
return ref unless ref.through_reflection?
|
93
|
+
resolve_closest_through_reflection( ref.through_reflection )
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# A resource creates a data store and instantiates a list of models that it wishes to load, building up the overall set of data that it will need.
|
2
|
+
# Once that is complete, the data set is iterated and a resultant view is generated.
|
3
|
+
module Praxis::Mapper
|
4
|
+
|
5
|
+
class Resource
|
6
|
+
extend Praxis::Finalizable
|
7
|
+
|
8
|
+
attr_accessor :record
|
9
|
+
|
10
|
+
@properties = {}
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_reader :model_map
|
14
|
+
attr_reader :properties
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO: also support an attribute of sorts on the versioned resource module. ie, V1::Resources.api_version.
|
18
|
+
# replacing the self.superclass == Praxis::Mapper::Resource condition below.
|
19
|
+
def self.inherited(klass)
|
20
|
+
super
|
21
|
+
|
22
|
+
klass.instance_eval do
|
23
|
+
# It is expected that each versioned set of resources
|
24
|
+
# will have a common Base class, and so should share
|
25
|
+
# a model_map
|
26
|
+
if self.superclass == Praxis::Mapper::Resource
|
27
|
+
@model_map = Hash.new
|
28
|
+
else
|
29
|
+
@model_map = self.superclass.model_map
|
30
|
+
end
|
31
|
+
|
32
|
+
@properties = self.superclass.properties.clone
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
#TODO: Take symbol/string and resolve the klass (but lazily, so we don't care about load order)
|
38
|
+
def self.model(klass=nil)
|
39
|
+
if klass
|
40
|
+
raise "Model #{klass.name} must be compatible with Praxis. Use ActiveModelCompat or similar compatability plugin." unless klass.methods.include?(:_praxis_associations)
|
41
|
+
@model = klass
|
42
|
+
self.model_map[klass] = self
|
43
|
+
else
|
44
|
+
@model
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.property(name, **options)
|
49
|
+
self.properties[name] = options
|
50
|
+
end
|
51
|
+
|
52
|
+
def self._finalize!
|
53
|
+
finalize_resource_delegates
|
54
|
+
define_model_accessors
|
55
|
+
|
56
|
+
super
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.finalize_resource_delegates
|
60
|
+
return unless @resource_delegates
|
61
|
+
|
62
|
+
@resource_delegates.each do |record_name, record_attributes|
|
63
|
+
record_attributes.each do |record_attribute|
|
64
|
+
self.define_resource_delegate(record_name, record_attribute)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def self.define_model_accessors
|
71
|
+
return if model.nil?
|
72
|
+
|
73
|
+
model._praxis_associations.each do |k,v|
|
74
|
+
unless self.instance_methods.include? k
|
75
|
+
define_model_association_accessor(k,v)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.for_record(record)
|
81
|
+
return record._resource if record._resource
|
82
|
+
|
83
|
+
if resource_class_for_record = model_map[record.class]
|
84
|
+
return record._resource = resource_class_for_record.new(record)
|
85
|
+
else
|
86
|
+
version = self.name.split("::")[0..-2].join("::")
|
87
|
+
resource_name = record.class.name.split("::").last
|
88
|
+
|
89
|
+
raise "No resource class corresponding to the model class '#{record.class}' is defined. (Did you forget to define '#{version}::#{resource_name}'?)"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
def self.wrap(records)
|
95
|
+
if records.nil?
|
96
|
+
return []
|
97
|
+
elsif( records.is_a?(Enumerable) )
|
98
|
+
return records.compact.map { |record| self.for_record(record) }
|
99
|
+
elsif ( records.respond_to?(:to_a) )
|
100
|
+
return records.to_a.compact.map { |record| self.for_record(record) }
|
101
|
+
else
|
102
|
+
return self.for_record(records)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
def self.get(condition)
|
108
|
+
record = self.model.get(condition)
|
109
|
+
|
110
|
+
self.wrap(record)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.all(condition={})
|
114
|
+
records = self.model.all(condition)
|
115
|
+
|
116
|
+
self.wrap(records)
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
def self.resource_delegates
|
121
|
+
@resource_delegates ||= {}
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.resource_delegate(spec)
|
125
|
+
spec.each do |resource_name, attributes|
|
126
|
+
resource_delegates[resource_name] = attributes
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Defines wrappers for model associations that return Resources
|
131
|
+
def self.define_model_association_accessor(name, association_spec)
|
132
|
+
association_model = association_spec.fetch(:model)
|
133
|
+
association_resource_class = model_map[association_model]
|
134
|
+
|
135
|
+
if association_resource_class
|
136
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
137
|
+
def #{name}
|
138
|
+
records = record.#{name}
|
139
|
+
return nil if records.nil?
|
140
|
+
@__#{name} ||= #{association_resource_class}.wrap(records)
|
141
|
+
end
|
142
|
+
RUBY
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.define_resource_delegate(resource_name, resource_attribute)
|
147
|
+
related_model = model._praxis_associations[resource_name][:model]
|
148
|
+
related_association = related_model._praxis_associations[resource_attribute]
|
149
|
+
|
150
|
+
if related_association
|
151
|
+
self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
|
152
|
+
else
|
153
|
+
self.define_delegation_for_related_attribute(resource_name, resource_attribute)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
def self.define_delegation_for_related_attribute(resource_name, resource_attribute)
|
159
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
160
|
+
def #{resource_attribute}
|
161
|
+
@__#{resource_attribute} ||= if (rec = self.#{resource_name})
|
162
|
+
rec.#{resource_attribute}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
RUBY
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
|
169
|
+
related_resource_class = model_map[related_association[:model]]
|
170
|
+
return unless related_resource_class
|
171
|
+
|
172
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
173
|
+
def #{resource_attribute}
|
174
|
+
@__#{resource_attribute} ||= if (rec = self.#{resource_name})
|
175
|
+
if (related = rec.#{resource_attribute})
|
176
|
+
#{related_resource_class.name}.wrap(related)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
RUBY
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.define_accessor(name)
|
184
|
+
if name.to_s =~ /\?/
|
185
|
+
ivar_name = "is_#{name.to_s[0..-2]}"
|
186
|
+
else
|
187
|
+
ivar_name = "#{name}"
|
188
|
+
end
|
189
|
+
|
190
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
191
|
+
def #{name}
|
192
|
+
return @__#{ivar_name} if defined? @__#{ivar_name}
|
193
|
+
@__#{ivar_name} = record.#{name}
|
194
|
+
end
|
195
|
+
RUBY
|
196
|
+
end
|
197
|
+
|
198
|
+
# TODO: this shouldn't be needed if we incorporate it with the properties of the mapper...
|
199
|
+
def self.filters_mapping(hash)
|
200
|
+
@_filter_query_builder_class = model._filter_query_builder_class.for(**hash)
|
201
|
+
end
|
202
|
+
|
203
|
+
def self._filter_query_builder_class
|
204
|
+
@_filter_query_builder_class
|
205
|
+
end
|
206
|
+
|
207
|
+
def self.craft_filter_query(base_query, filters:) # rubocop:disable Metrics/AbcSize
|
208
|
+
if filters && _filter_query_builder_class
|
209
|
+
base_query = _filter_query_builder_class.new(query: base_query, model: model).build_clause(filters)
|
210
|
+
end
|
211
|
+
|
212
|
+
base_query
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.craft_field_selection_query(base_query, selectors:) # rubocop:disable Metrics/AbcSize
|
216
|
+
if selectors && model._field_selector_query_builder_class
|
217
|
+
debug = Praxis::Application.instance.config.mapper.debug_queries
|
218
|
+
base_query = model._field_selector_query_builder_class.new(query: base_query, selectors: selectors).generate(debug: debug)
|
219
|
+
end
|
220
|
+
|
221
|
+
base_query
|
222
|
+
end
|
223
|
+
|
224
|
+
def initialize(record)
|
225
|
+
@record = record
|
226
|
+
end
|
227
|
+
|
228
|
+
def respond_to_missing?(name,*)
|
229
|
+
@record.respond_to?(name) || super
|
230
|
+
end
|
231
|
+
|
232
|
+
def method_missing(name,*args)
|
233
|
+
if @record.respond_to?(name)
|
234
|
+
self.class.define_accessor(name)
|
235
|
+
self.send(name)
|
236
|
+
else
|
237
|
+
super
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module Praxis::Mapper
|
2
|
+
|
3
|
+
class SelectorGeneratorNode
|
4
|
+
attr_reader :select, :model, :resource, :tracks
|
5
|
+
|
6
|
+
def initialize(resource)
|
7
|
+
@resource = resource
|
8
|
+
|
9
|
+
@select = Set.new
|
10
|
+
@select_star = false
|
11
|
+
@tracks = Hash.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(fields)
|
15
|
+
fields.each do |name, field|
|
16
|
+
map_property(name, field)
|
17
|
+
end
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def map_property(name, fields)
|
22
|
+
praxis_compat_model = resource.model && resource.model.respond_to?(:_praxis_associations)
|
23
|
+
if resource.properties.key?(name)
|
24
|
+
add_property(name, fields)
|
25
|
+
elsif praxis_compat_model && resource.model._praxis_associations.key?(name)
|
26
|
+
add_association(name, fields)
|
27
|
+
else
|
28
|
+
add_select(name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_association(name, fields)
|
33
|
+
|
34
|
+
association = resource.model._praxis_associations.fetch(name) do
|
35
|
+
raise "missing association for #{resource} with name #{name}"
|
36
|
+
end
|
37
|
+
associated_resource = resource.model_map[association[:model]]
|
38
|
+
unless associated_resource
|
39
|
+
raise "Whoops! could not find a resource associated with model #{association[:model]} (root resource #{resource})"
|
40
|
+
end
|
41
|
+
# Add the required columns in this model to make sure the association can be loaded
|
42
|
+
association[:local_key_columns].each {|col| add_select(col) }
|
43
|
+
|
44
|
+
node = SelectorGeneratorNode.new(associated_resource)
|
45
|
+
if association[:remote_key_columns].nil?
|
46
|
+
binding.pry
|
47
|
+
puts association
|
48
|
+
end
|
49
|
+
unless association[:remote_key_columns].empty?
|
50
|
+
# Make sure we add the required columns for this association to the remote model query
|
51
|
+
fields = {} if fields == true
|
52
|
+
new_fields_as_hash = association[:remote_key_columns].each_with_object({}) do|name, hash|
|
53
|
+
hash[name] = true
|
54
|
+
end
|
55
|
+
fields.merge!(new_fields_as_hash)
|
56
|
+
end
|
57
|
+
|
58
|
+
node.add(fields) unless fields == true
|
59
|
+
|
60
|
+
self.merge_track(name, node)
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_select(name)
|
64
|
+
return @select_star = true if name == :*
|
65
|
+
return if @select_star
|
66
|
+
@select.add name
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_property(name, fields)
|
70
|
+
dependencies = resource.properties[name][:dependencies]
|
71
|
+
# Always add the underlying association if we're overriding the name...
|
72
|
+
praxis_compat_model = resource.model && resource.model.respond_to?(:_praxis_associations)
|
73
|
+
if praxis_compat_model && resource.model._praxis_associations.key?(name)
|
74
|
+
add_association(name, fields)
|
75
|
+
end
|
76
|
+
if dependencies
|
77
|
+
dependencies.each do |dependency|
|
78
|
+
# To detect recursion, let's allow mapping depending fields to the same name of the property
|
79
|
+
# but properly detecting if it's a real association...in which case we've already added it above
|
80
|
+
if dependency == name
|
81
|
+
unless praxis_compat_model && resource.model._praxis_associations.key?(name)
|
82
|
+
add_select(name)
|
83
|
+
end
|
84
|
+
else
|
85
|
+
apply_dependency(dependency)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
head, *tail = resource.properties[name][:through]
|
91
|
+
return if head.nil?
|
92
|
+
|
93
|
+
new_fields = tail.reverse.inject(fields) do |thing, step|
|
94
|
+
{step => thing}
|
95
|
+
end
|
96
|
+
|
97
|
+
add_association(head, new_fields)
|
98
|
+
end
|
99
|
+
|
100
|
+
def apply_dependency(dependency)
|
101
|
+
case dependency
|
102
|
+
when Symbol
|
103
|
+
map_property(dependency, true)
|
104
|
+
when String
|
105
|
+
head, tail = dependency.split('.').collect(&:to_sym)
|
106
|
+
raise "String dependencies can not be singular" if tail.nil?
|
107
|
+
|
108
|
+
add_association(head, {tail => true})
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def merge_track( track_name, node )
|
113
|
+
raise "Cannot merge another node for association #{track_name}: incompatible model" unless node.model == self.model
|
114
|
+
|
115
|
+
existing = self.tracks[track_name]
|
116
|
+
if existing
|
117
|
+
node.select.each do|col_name|
|
118
|
+
existing.add_select(col_name)
|
119
|
+
end
|
120
|
+
node.tracks.each do |name, n|
|
121
|
+
existing.merge_track(name, n)
|
122
|
+
end
|
123
|
+
else
|
124
|
+
self.tracks[track_name] = node
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def dump
|
129
|
+
hash = {}
|
130
|
+
hash[:model] = resource.model
|
131
|
+
if !@select.empty? || @select_star
|
132
|
+
hash[:columns] = @select_star ? [ :* ] : @select.to_a
|
133
|
+
end
|
134
|
+
unless @tracks.empty?
|
135
|
+
hash[:tracks] = @tracks.each_with_object({}) {|(name, node), hash| hash[name] = node.dump }
|
136
|
+
end
|
137
|
+
hash
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Generates a set of selectors given a resource and
|
142
|
+
# list of resource attributes.
|
143
|
+
class SelectorGenerator
|
144
|
+
# Entry point
|
145
|
+
def add(resource, fields)
|
146
|
+
@root = SelectorGeneratorNode.new(resource)
|
147
|
+
@root.add(fields)
|
148
|
+
end
|
149
|
+
|
150
|
+
def selectors
|
151
|
+
@root
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|