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
@@ -0,0 +1,76 @@
1
+ require 'active_support/concern'
2
+
3
+
4
+ module Praxis::Mapper
5
+ module SequelCompat
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attr_accessor :_resource
10
+ end
11
+
12
+ module ClassMethods
13
+ def _filter_query_builder_class
14
+ Praxis::Extensions::SequelFilterQueryBuilder
15
+ end
16
+
17
+ def _field_selector_query_builder_class
18
+ Praxis::Extensions::FieldSelection::SequelQuerySelector
19
+ end
20
+
21
+ def _praxis_associations
22
+ orig = self.association_reflections.clone
23
+ orig.each do |k,v|
24
+ v[:model] = v.associated_class
25
+ v[:local_key_columns] = local_columns_used_for_the_association(v[:type], v)
26
+ v[:remote_key_columns] = remote_columns_used_for_the_association(v[:type], v)
27
+ if v.respond_to?(:primary_key)
28
+ v[:primary_key] = v.primary_key
29
+ else
30
+ # FIXME: figure out exactly what to do here.
31
+ # not super critical, as we can't track these associations
32
+ # directly, but it would be nice to traverse these
33
+ # properly.
34
+ v[:primary_key] = :unsupported
35
+ end
36
+ end
37
+ orig
38
+ end
39
+
40
+ private
41
+ def local_columns_used_for_the_association(type, assoc_reflection)
42
+ case type
43
+ when :one_to_many
44
+ # The associated table (or middle table if many to many) will point to us by PK
45
+ assoc_reflection[:primary_key_columns]
46
+ when :many_to_one
47
+ # We have the FKs to the associated model
48
+ assoc_reflection[:keys]
49
+ when :many_to_many
50
+ # The middle table if many to many) will point to us by key (usually the PK, but not always)
51
+ assoc_reflection[:left_primary_keys]
52
+ else
53
+ raise "association type #{type} not supported"
54
+ end
55
+ end
56
+
57
+ def remote_columns_used_for_the_association(type, assoc_reflection)
58
+ case type
59
+ when :one_to_many
60
+ # The columns in the associated table that will point back to the original association
61
+ assoc_reflection[:keys]
62
+ when :many_to_one
63
+ # The columns in the associated table that the children will point to (usually the PK, but not always) ??
64
+ [assoc_reflection.associated_class.primary_key]
65
+ when :many_to_many
66
+ # The middle table if many to many will point to us by key (usually the PK, but not always) ??
67
+ [assoc_reflection.associated_class.primary_key]
68
+ else
69
+ raise "association type #{type} not supported"
70
+ end
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+ end
@@ -146,7 +146,8 @@ module Praxis
146
146
  if self.parameters.empty?
147
147
  self
148
148
  else
149
- MediaTypeIdentifier.load(type: self.type, subtype: self.subtype, suffix: self.suffix)
149
+ val = {type: self.type, subtype: self.subtype, suffix: self.suffix}
150
+ MediaTypeIdentifier.load(val)
150
151
  end
151
152
  end
152
153
 
@@ -2,39 +2,37 @@ module Praxis
2
2
  class MiddlewareApp
3
3
 
4
4
  attr_reader :target
5
+
5
6
  # Initialize the application instance with the desired args, and return the wrapping class.
6
7
  def self.for( **args )
7
8
  Class.new(self) do
8
- class << self
9
- attr_accessor :app_instance
10
- attr_reader :app_name, :skip_registration
11
- end
12
- @app_name = args.delete(:name)
13
- @skip_registration = args.delete(:skip_registration) || false
14
9
  @args = args
15
- @app_instance = nil
16
-
10
+ @setup_done = false
17
11
  def self.name
18
12
  'MiddlewareApp'
19
13
  end
20
14
  def self.args
21
15
  @args
22
16
  end
17
+ def self.setup_done
18
+ @setup_done
19
+ end
23
20
  def self.setup
24
- app_instance.setup(**args)
21
+ @setup_done = true
22
+ Praxis::Application.instance.setup(**@args)
25
23
  end
26
24
  end
27
- end
25
+ end
28
26
 
29
27
  def initialize( inner )
30
28
  @target = inner
31
- self.class.app_instance = Praxis::Application.new(name: self.class.app_name, skip_registration: self.class.skip_registration)
29
+ @setup_done = false
32
30
  end
33
-
31
+
34
32
  def call(env)
35
- # NOTE: Need to make sure somebody has properly called the setup above before this is called
36
- #@app_instance ||= Praxis::Application.new.setup(**self.class.args) #I Think that's not right at all...
37
- result = self.class.app_instance.call(env)
33
+ self.class.setup unless self.class.setup_done
34
+
35
+ result = Praxis::Application.instance.call(env)
38
36
 
39
37
  unless ( [404,405].include?(result[0].to_i) && result[1]['X-Cascade'] == 'pass' )
40
38
  # Respect X-Cascade header if it doesn't specify 'pass'
@@ -10,7 +10,6 @@ module Praxis
10
10
  attr_accessor :headers_attribute
11
11
  attr_accessor :filename_attribute
12
12
  attr_accessor :default_handler
13
- attr_accessor :application
14
13
 
15
14
  def self.check_option!(name, definition)
16
15
  case name
@@ -78,8 +77,7 @@ module Praxis
78
77
  @name = name
79
78
  @body = body
80
79
  @headers = headers
81
- @application = Praxis::Application.current_instance
82
- @default_handler = application.handlers['json']
80
+ @default_handler = Praxis::Application.instance.handlers['json']
83
81
 
84
82
  if content_type.nil?
85
83
  self.content_type = 'text/plain'
@@ -214,7 +212,7 @@ module Praxis
214
212
  end
215
213
 
216
214
  def handler
217
- handlers = application.handlers
215
+ handlers = Praxis::Application.instance.handlers
218
216
  (content_type && handlers[content_type.handler_name]) || @default_handler
219
217
  end
220
218
 
@@ -251,7 +249,7 @@ module Praxis
251
249
 
252
250
  # and return that one if it already corresponds to a registered handler
253
251
  # otherwise, add the encoding
254
- if application.handlers.include?(pick.handler_name)
252
+ if Praxis::Application.instance.handlers.include?(pick.handler_name)
255
253
  return pick
256
254
  else
257
255
  return pick + handler_name
@@ -21,7 +21,7 @@ module Praxis
21
21
  end
22
22
 
23
23
  def self.instrument(name, payload = {}, &block)
24
- ActiveSupport::Notifications.instrument(name, payload, &block)
24
+ ActiveSupport::Notifications.instrument(name, **payload, &block)
25
25
  end
26
26
 
27
27
  def self.subscribe(*args, &block)
@@ -0,0 +1,64 @@
1
+ require 'singleton'
2
+ require 'praxis/extensions/attribute_filtering/filtering_params'
3
+
4
+ module Praxis
5
+ module Plugins
6
+ module MapperPlugin
7
+ include Praxis::PluginConcern
8
+
9
+ class Plugin < Praxis::Plugin
10
+ include Singleton
11
+
12
+ def config_key
13
+ :mapper
14
+ end
15
+
16
+ def load_config!
17
+ {} # override the default one, since we don't necessarily want to configure it via a yaml file.
18
+ end
19
+
20
+ def prepare_config!(node)
21
+ node.attributes do
22
+ attribute :debug_queries, Attributor::Boolean, default: false,
23
+ description: 'Weather or not to log debug information about queries executed in the build_query automation module'
24
+ end
25
+ end
26
+ end
27
+
28
+ module Controller
29
+ extend ActiveSupport::Concern
30
+
31
+ included do
32
+ include Praxis::Extensions::FieldExpansion
33
+ end
34
+
35
+ def set_selectors
36
+ return unless self.media_type.respond_to?(:domain_model) &&
37
+ self.media_type.domain_model < Praxis::Mapper::Resource
38
+
39
+ resolved = Praxis::MediaType::FieldResolver.resolve(self.media_type, self.expanded_fields)
40
+ selector_generator.add(self.media_type.domain_model, resolved)
41
+ end
42
+
43
+ def build_query(base_query) # rubocop:disable Metrics/AbcSize
44
+ domain_model = self.media_type&.domain_model
45
+ raise "No domain model defined for #{self.name}. Cannot use the attribute filtering helpers without it" unless domain_model
46
+
47
+ filters = request.params.filters if request.params&.respond_to?(:filters)
48
+ base_query = domain_model.craft_filter_query( base_query , filters: filters )
49
+
50
+ base_query = domain_model.craft_field_selection_query(base_query, selectors: selector_generator.selectors)
51
+
52
+ # TODO: handle pagination and ordering
53
+ base_query
54
+ end
55
+
56
+ def selector_generator
57
+ @selector_generator ||= Praxis::Mapper::SelectorGenerator.new
58
+ end
59
+
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -1,7 +1,7 @@
1
1
  module Praxis
2
2
 
3
3
  class Request < Praxis.request_superclass
4
- attr_reader :env, :query, :praxis_instance
4
+ attr_reader :env, :query
5
5
  attr_accessor :route_params, :action
6
6
 
7
7
  PATH_VERSION_PREFIX = "/v".freeze
@@ -14,11 +14,10 @@ module Praxis
14
14
  API_NO_VERSION_NAME = 'n/a'.freeze
15
15
  VERSION_USING_DEFAULTS = [:header, :params].freeze
16
16
 
17
- def initialize(env, args={})
17
+ def initialize(env)
18
18
  @env = env
19
19
  @query = Rack::Utils.parse_nested_query(env[QUERY_STRING_NAME])
20
20
  @route_params = {}
21
- @praxis_instance = args[:application]
22
21
  end
23
22
 
24
23
  # Determine the content type of this request as indicated by the Content-Type header.
@@ -87,8 +86,8 @@ module Praxis
87
86
  PATH_VERSION_MATCHER = %r{^#{self.path_version_prefix}(?<version>[^\/]+)\/}.freeze
88
87
 
89
88
  def path_version_matcher
90
- if praxis_instance.versioning_scheme == :path
91
- matcher = Mustermann.new(praxis_instance.api_definition.info.base_path + '*')
89
+ if Application.instance.versioning_scheme == :path
90
+ matcher = Mustermann.new(ApiDefinition.instance.info.base_path + '*')
92
91
  matcher.params(self.path)[API_VERSION_PARAM_NAME]
93
92
  else
94
93
  PATH_VERSION_MATCHER.match(self.path)[:version]
@@ -97,7 +96,8 @@ module Praxis
97
96
 
98
97
  def version
99
98
  result = nil
100
- Array(praxis_instance.versioning_scheme).find do |mode|
99
+
100
+ Array(Application.instance.versioning_scheme).find do |mode|
101
101
  case mode
102
102
  when :header;
103
103
  result = env[API_VERSION_HEADER_NAME]
@@ -128,7 +128,8 @@ module Praxis
128
128
  def load_payload(context)
129
129
  return unless action.payload
130
130
  return if content_type.nil?
131
- raw = if (handler = praxis_instance.handlers[content_type.handler_name])
131
+
132
+ raw = if (handler = Praxis::Application.instance.handlers[content_type.handler_name])
132
133
  handler.parse(self.raw_payload)
133
134
  else
134
135
  # TODO is this a good default?
@@ -161,6 +162,12 @@ module Praxis
161
162
  @unmatched_versions ||= Set.new
162
163
  end
163
164
 
165
+ # Override the inspect instance method of a request, as, by default, the kernel inspect will go nuts
166
+ # traversing the action and app_instance and therefore all associated instance variables reachable through that
167
+ def inspect
168
+ "'@env' => #{@env.inspect},\n'@headers' => #{@headers.inspect},\n'@params' => #{@params.inspect},\n'@query' => #{@query.inspect}"
169
+ end
170
+
164
171
  end
165
172
 
166
173
  end
@@ -8,9 +8,8 @@ module Praxis
8
8
 
9
9
  response.handle
10
10
 
11
- config = Application.current_instance.config
12
- if config.praxis.validate_responses == true
13
- validate_body = config.praxis.validate_response_bodies
11
+ if Application.instance.config.praxis.validate_responses == true
12
+ validate_body = Application.instance.config.praxis.validate_response_bodies
14
13
 
15
14
  response.validate(action, validate_body: validate_body)
16
15
  end
@@ -8,15 +8,11 @@ module Praxis
8
8
  DEFAULT_RESOURCE_HREF_ACTION = :show
9
9
 
10
10
  included do
11
- # Store the attached (i.e., current) Praxis App instance into the resource definition for easy retrieval later
12
- @application = Application.instance
13
- @application.resource_definitions << self
14
-
15
11
  @version = 'n/a'.freeze
16
12
  @actions = Hash.new
17
13
  @responses = Hash.new
18
14
 
19
- @action_defaults = Trait.new &ResourceDefinition.generate_defaults_block(application: @application)
15
+ @action_defaults = Trait.new &ResourceDefinition.generate_defaults_block
20
16
 
21
17
  @version_options = {}
22
18
  @metadata = {}
@@ -37,12 +33,13 @@ module Praxis
37
33
 
38
34
  @on_finalize = Array.new
39
35
 
40
-
36
+ Application.instance.resource_definitions << self
41
37
  end
42
38
 
43
- def self.generate_defaults_block( version: nil, application:)
39
+ def self.generate_defaults_block( version: nil )
40
+
44
41
  # Ensure we inherit any base params defined in the API definition for the passed in version
45
- base_attributes = if (base_params = application.api_definition.info(version).base_params)
42
+ base_attributes = if (base_params = ApiDefinition.instance.info(version).base_params)
46
43
  base_params.attributes
47
44
  else
48
45
  {}
@@ -52,15 +49,15 @@ module Praxis
52
49
  unless base_attributes.empty?
53
50
  params do
54
51
  base_attributes.each do |base_name, base_attribute|
55
- attribute base_name, base_attribute.type, base_attribute.options
52
+ attribute base_name, base_attribute.type, **base_attribute.options
56
53
  end
57
54
  end
58
55
  end
59
56
  end
60
57
  end
61
58
 
62
- def self.finalize!(application: )
63
- application.resource_definitions.each do |resource_definition|
59
+ def self.finalize!
60
+ Application.instance.resource_definitions.each do |resource_definition|
64
61
  while (block = resource_definition.on_finalize.shift)
65
62
  block.call
66
63
  end
@@ -76,7 +73,6 @@ module Praxis
76
73
  attr_reader :traits
77
74
  attr_reader :version_prefix
78
75
  attr_reader :parent_prefix
79
- attr_reader :application
80
76
 
81
77
  # opaque hash of user-defined medata, used to decorate the definition,
82
78
  # and also available in the generated JSON documents
@@ -91,9 +87,9 @@ module Praxis
91
87
  @display_name = string
92
88
  end
93
89
 
94
- def on_finalize
90
+ def on_finalize(&block)
95
91
  if block_given?
96
- @on_finalize << Proc.new
92
+ @on_finalize << proc(&block)
97
93
  end
98
94
 
99
95
  @on_finalize
@@ -172,7 +168,7 @@ module Praxis
172
168
 
173
169
  parent_attribute = parent_action.params.attributes[parent_name]
174
170
 
175
- attribute name, parent_attribute.type, parent_attribute.options
171
+ attribute name, parent_attribute.type, **parent_attribute.options
176
172
  end
177
173
  end
178
174
  end
@@ -203,7 +199,7 @@ module Praxis
203
199
  end
204
200
  end
205
201
 
206
- @action_defaults.instance_eval &ResourceDefinition.generate_defaults_block( version: version, application: self.application)
202
+ @action_defaults.instance_eval &ResourceDefinition.generate_defaults_block( version: version )
207
203
  end
208
204
 
209
205
 
@@ -225,7 +221,7 @@ module Praxis
225
221
  end
226
222
 
227
223
  def to_href( params )
228
- canonical_path.primary_route.path.expand(params)
224
+ canonical_path.primary_route.path.expand(params.transform_values(&:to_s))
229
225
  end
230
226
 
231
227
  def parse_href(path)
@@ -242,10 +238,10 @@ module Praxis
242
238
  end
243
239
 
244
240
  def trait(trait_name)
245
- unless self.application.api_definition.traits.has_key? trait_name
241
+ unless ApiDefinition.instance.traits.has_key? trait_name
246
242
  raise Exceptions::InvalidTrait.new("Trait #{trait_name} not found in the system")
247
243
  end
248
- trait = self.application.api_definition.traits.fetch(trait_name)
244
+ trait = ApiDefinition.instance.traits.fetch(trait_name)
249
245
  @traits << trait_name
250
246
  end
251
247
  alias_method :use, :trait
@@ -63,23 +63,24 @@ module Praxis
63
63
  self.class.response_name
64
64
  end
65
65
 
66
- def format!(config:)
66
+ def format!
67
67
  end
68
68
 
69
- def encode!(handlers:)
69
+ def encode!
70
70
  case @body
71
71
  when Hash, Array
72
72
  # response payload is structured data; transform it into an entity using the handler
73
73
  # implied by the response's media type. If no handler is registered for this
74
74
  # name, assume JSON as a default handler.
75
+ handlers = Praxis::Application.instance.handlers
75
76
  handler = (content_type && handlers[content_type.handler_name]) || handlers['json']
76
77
  @body = handler.generate(@body)
77
78
  end
78
79
  end
79
80
 
80
- def finish(application:)
81
- format!(config: application.config)
82
- encode!(handlers: application.handlers)
81
+ def finish
82
+ format!
83
+ encode!
83
84
 
84
85
  @body = Array(@body)
85
86