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.
Files changed (96) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +5 -20
  3. data/CHANGELOG.md +328 -323
  4. data/lib/praxis.rb +13 -9
  5. data/lib/praxis/action_definition.rb +8 -10
  6. data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
  7. data/lib/praxis/api_definition.rb +27 -44
  8. data/lib/praxis/api_general_info.rb +2 -3
  9. data/lib/praxis/application.rb +15 -142
  10. data/lib/praxis/bootloader.rb +1 -2
  11. data/lib/praxis/bootloader_stages/environment.rb +13 -0
  12. data/lib/praxis/config.rb +1 -1
  13. data/lib/praxis/controller.rb +0 -2
  14. data/lib/praxis/dispatcher.rb +4 -6
  15. data/lib/praxis/docs/generator.rb +8 -18
  16. data/lib/praxis/docs/link_builder.rb +1 -1
  17. data/lib/praxis/error_handler.rb +5 -5
  18. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +1 -1
  19. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +1 -1
  20. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
  21. data/lib/praxis/extensions/field_selection.rb +1 -12
  22. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +28 -34
  23. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +35 -39
  24. data/lib/praxis/extensions/rendering.rb +1 -1
  25. data/lib/praxis/file_group.rb +1 -1
  26. data/lib/praxis/handlers/xml.rb +1 -1
  27. data/lib/praxis/mapper/active_model_compat.rb +98 -0
  28. data/lib/praxis/mapper/resource.rb +242 -0
  29. data/lib/praxis/mapper/selector_generator.rb +154 -0
  30. data/lib/praxis/mapper/sequel_compat.rb +76 -0
  31. data/lib/praxis/media_type_identifier.rb +2 -1
  32. data/lib/praxis/middleware_app.rb +13 -15
  33. data/lib/praxis/multipart/part.rb +3 -5
  34. data/lib/praxis/notifications.rb +1 -1
  35. data/lib/praxis/plugins/mapper_plugin.rb +64 -0
  36. data/lib/praxis/request.rb +14 -7
  37. data/lib/praxis/request_stages/response.rb +2 -3
  38. data/lib/praxis/resource_definition.rb +15 -19
  39. data/lib/praxis/response.rb +6 -5
  40. data/lib/praxis/response_definition.rb +5 -7
  41. data/lib/praxis/response_template.rb +3 -4
  42. data/lib/praxis/responses/http.rb +36 -0
  43. data/lib/praxis/responses/internal_server_error.rb +12 -3
  44. data/lib/praxis/responses/multipart_ok.rb +11 -4
  45. data/lib/praxis/responses/validation_error.rb +10 -1
  46. data/lib/praxis/route.rb +1 -1
  47. data/lib/praxis/router.rb +3 -3
  48. data/lib/praxis/routing_config.rb +1 -1
  49. data/lib/praxis/tasks/api_docs.rb +2 -10
  50. data/lib/praxis/tasks/routes.rb +0 -1
  51. data/lib/praxis/trait.rb +1 -1
  52. data/lib/praxis/types/media_type_common.rb +2 -2
  53. data/lib/praxis/types/multipart.rb +1 -1
  54. data/lib/praxis/types/multipart_array.rb +2 -2
  55. data/lib/praxis/types/multipart_array/part_definition.rb +1 -1
  56. data/lib/praxis/version.rb +1 -1
  57. data/praxis.gemspec +11 -9
  58. data/spec/functional_spec.rb +0 -1
  59. data/spec/praxis/action_definition_spec.rb +16 -27
  60. data/spec/praxis/api_definition_spec.rb +8 -13
  61. data/spec/praxis/api_general_info_spec.rb +8 -3
  62. data/spec/praxis/application_spec.rb +8 -14
  63. data/spec/praxis/collection_spec.rb +3 -2
  64. data/spec/praxis/config_spec.rb +2 -2
  65. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +106 -0
  66. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +147 -0
  67. data/spec/praxis/extensions/field_selection/support/spec_resources_active_model.rb +130 -0
  68. data/spec/praxis/extensions/field_selection/support/spec_resources_sequel.rb +106 -0
  69. data/spec/praxis/handlers/xml_spec.rb +2 -2
  70. data/spec/praxis/mapper/resource_spec.rb +169 -0
  71. data/spec/praxis/mapper/selector_generator_spec.rb +325 -0
  72. data/spec/praxis/media_type_spec.rb +0 -10
  73. data/spec/praxis/middleware_app_spec.rb +16 -10
  74. data/spec/praxis/request_spec.rb +7 -17
  75. data/spec/praxis/request_stages/action_spec.rb +8 -1
  76. data/spec/praxis/request_stages/validate_spec.rb +1 -1
  77. data/spec/praxis/resource_definition_spec.rb +10 -12
  78. data/spec/praxis/response_definition_spec.rb +12 -26
  79. data/spec/praxis/response_spec.rb +6 -13
  80. data/spec/praxis/responses/internal_server_error_spec.rb +5 -2
  81. data/spec/praxis/router_spec.rb +5 -9
  82. data/spec/spec_app/app/controllers/instances.rb +1 -1
  83. data/spec/spec_app/config.ru +6 -1
  84. data/spec/spec_app/config/environment.rb +3 -21
  85. data/spec/spec_helper.rb +13 -17
  86. data/spec/support/be_deep_equal_matcher.rb +39 -0
  87. data/spec/support/spec_resources.rb +124 -0
  88. metadata +74 -53
  89. data/lib/praxis/extensions/attribute_filtering.rb +0 -28
  90. data/lib/praxis/extensions/attribute_filtering/query_builder.rb +0 -39
  91. data/lib/praxis/extensions/mapper_selectors.rb +0 -16
  92. data/lib/praxis/media_type_collection.rb +0 -127
  93. data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
  94. data/spec/praxis/media_type_collection_spec.rb +0 -157
  95. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
  96. 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.current_instance.validation_handler.handle!(
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,
@@ -7,7 +7,7 @@ module Praxis
7
7
  def initialize(base, &block)
8
8
  if base.nil?
9
9
  raise ArgumentError, "base must not be nil." \
10
- "Have you forgot to call 'setup' on the Praxis application instance?"
10
+ "Are you missing a call Praxis::Application.instance.setup?"
11
11
  end
12
12
 
13
13
 
@@ -60,7 +60,7 @@ module Praxis
60
60
  when "symbol"
61
61
  return node.content.to_sym
62
62
  when "decimal"
63
- return BigDecimal.new(node.content)
63
+ return BigDecimal(node.content)
64
64
  when "float"
65
65
  return Float(node.content)
66
66
  when "boolean"
@@ -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