praxis 0.21 → 0.22.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +20 -12
  3. data/CHANGELOG.md +24 -0
  4. data/CONTRIBUTING.md +4 -4
  5. data/README.md +11 -9
  6. data/lib/api_browser/app/js/directives/attribute_table.js +2 -1
  7. data/lib/api_browser/app/js/directives/conditional_requirements.js +13 -0
  8. data/lib/api_browser/app/js/directives/type_placeholder.js +10 -1
  9. data/lib/api_browser/app/js/factories/normalize_attributes.js +4 -2
  10. data/lib/api_browser/app/js/factories/template_for.js +5 -2
  11. data/lib/api_browser/app/js/filters/has_requirement.js +14 -0
  12. data/lib/api_browser/app/js/filters/tag_requirement.js +13 -0
  13. data/lib/api_browser/app/sass/praxis.scss +11 -0
  14. data/lib/api_browser/app/views/action.html +2 -2
  15. data/lib/api_browser/app/views/directives/attribute_description/member_options.html +2 -2
  16. data/lib/api_browser/app/views/directives/attribute_table.html +1 -1
  17. data/lib/api_browser/app/views/type.html +1 -1
  18. data/lib/api_browser/app/views/type/details.html +2 -2
  19. data/lib/api_browser/app/views/types/embedded/array.html +2 -0
  20. data/lib/api_browser/app/views/types/embedded/default.html +3 -1
  21. data/lib/api_browser/app/views/types/embedded/requirements.html +6 -0
  22. data/lib/api_browser/app/views/types/embedded/single_req.html +9 -0
  23. data/lib/api_browser/app/views/types/embedded/struct.html +14 -2
  24. data/lib/api_browser/app/views/types/standalone/array.html +1 -1
  25. data/lib/api_browser/app/views/types/standalone/struct.html +2 -1
  26. data/lib/api_browser/package.json +1 -1
  27. data/lib/praxis.rb +8 -6
  28. data/lib/praxis/action_definition.rb +9 -7
  29. data/lib/praxis/api_definition.rb +44 -27
  30. data/lib/praxis/api_general_info.rb +3 -2
  31. data/lib/praxis/application.rb +139 -20
  32. data/lib/praxis/bootloader.rb +2 -4
  33. data/lib/praxis/bootloader_stages/environment.rb +0 -13
  34. data/lib/praxis/controller.rb +2 -0
  35. data/lib/praxis/dispatcher.rb +16 -10
  36. data/lib/praxis/docs/generator.rb +20 -9
  37. data/lib/praxis/docs/link_builder.rb +1 -1
  38. data/lib/praxis/error_handler.rb +5 -5
  39. data/lib/praxis/extensions/attribute_filtering.rb +28 -0
  40. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +180 -0
  41. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +273 -0
  42. data/lib/praxis/extensions/attribute_filtering/query_builder.rb +39 -0
  43. data/lib/praxis/extensions/field_selection.rb +3 -0
  44. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +57 -0
  45. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +65 -0
  46. data/lib/praxis/extensions/rails_compat.rb +2 -0
  47. data/lib/praxis/extensions/rails_compat/request_methods.rb +19 -0
  48. data/lib/praxis/extensions/rendering.rb +1 -1
  49. data/lib/praxis/file_group.rb +1 -1
  50. data/lib/praxis/middleware_app.rb +26 -6
  51. data/lib/praxis/multipart/parser.rb +14 -2
  52. data/lib/praxis/multipart/part.rb +5 -3
  53. data/lib/praxis/plugins/praxis_mapper_plugin.rb +2 -2
  54. data/lib/praxis/plugins/rails_plugin.rb +104 -0
  55. data/lib/praxis/request.rb +8 -9
  56. data/lib/praxis/request_stages/response.rb +3 -2
  57. data/lib/praxis/request_superclassing.rb +11 -0
  58. data/lib/praxis/resource_definition.rb +14 -10
  59. data/lib/praxis/response.rb +6 -7
  60. data/lib/praxis/response_definition.rb +7 -5
  61. data/lib/praxis/response_template.rb +4 -3
  62. data/lib/praxis/responses/http.rb +0 -36
  63. data/lib/praxis/responses/internal_server_error.rb +3 -12
  64. data/lib/praxis/responses/multipart_ok.rb +4 -11
  65. data/lib/praxis/responses/validation_error.rb +1 -10
  66. data/lib/praxis/router.rb +3 -3
  67. data/lib/praxis/tasks/api_docs.rb +10 -2
  68. data/lib/praxis/tasks/routes.rb +1 -0
  69. data/lib/praxis/version.rb +1 -1
  70. data/praxis.gemspec +4 -5
  71. data/spec/functional_spec.rb +4 -6
  72. data/spec/praxis/action_definition_spec.rb +26 -15
  73. data/spec/praxis/api_definition_spec.rb +13 -8
  74. data/spec/praxis/api_general_info_spec.rb +3 -8
  75. data/spec/praxis/application_spec.rb +13 -7
  76. data/spec/praxis/middleware_app_spec.rb +24 -10
  77. data/spec/praxis/request_spec.rb +17 -7
  78. data/spec/praxis/request_stages/validate_spec.rb +1 -1
  79. data/spec/praxis/resource_definition_spec.rb +12 -10
  80. data/spec/praxis/response_definition_spec.rb +22 -5
  81. data/spec/praxis/response_spec.rb +12 -5
  82. data/spec/praxis/responses/internal_server_error_spec.rb +4 -7
  83. data/spec/praxis/responses/validation_error_spec.rb +2 -2
  84. data/spec/praxis/router_spec.rb +8 -4
  85. data/spec/spec_app/config.ru +1 -6
  86. data/spec/spec_helper.rb +3 -3
  87. data/tasks/thor/templates/generator/empty_app/Gemfile +3 -3
  88. metadata +36 -32
  89. data/.ruby-version +0 -1
  90. data/lib/praxis/stats.rb +0 -113
  91. data/spec/praxis/stats_spec.rb +0 -9
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'active_record_filter_query_builder'
3
+
4
+ module Praxis
5
+ module Extensions
6
+ module QueryBuilder
7
+ # To include in a resource object...
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # TODO: this shouldn't be needed if we incorporate it with the properties of the mapper...
12
+ def self.filters_mapping(hash)
13
+ @query_builder_class = ActiveRecordFilterQueryBuilder.for(**hash)
14
+ end
15
+
16
+ def self.query_builder_class
17
+ @query_builder_class
18
+ end
19
+
20
+ def self.craft_query(base_query, filters) # rubocop:disable Metrics/AbcSize
21
+ # Assume QueryBuilder
22
+ if query_builder_class
23
+ unless query_builder_class.ancestors.include?(ActiveRecordFilterQueryBuilder)
24
+ raise ArgumentError, ':query_builder_class must a class extending FilterQueryBuilder'
25
+ end
26
+
27
+ if filters && query_builder_class
28
+ base_query = query_builder_class.new(query: base_query, model: model ).build_clause(filters)
29
+ end
30
+ # puts "FILTERS_QUERY: #{filters_query.sql}"
31
+ end
32
+
33
+ base_query
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -1,6 +1,9 @@
1
1
  require 'attributor/extras/field_selector'
2
2
 
3
3
  require 'praxis/extensions/field_selection/field_selector'
4
+ # TODO: we should conditionally require it based on what ORM/s we want...
5
+ require 'praxis/extensions/field_selection/active_record_query_selector'
6
+
4
7
 
5
8
  module Praxis
6
9
  module Extensions
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ module Praxis
3
+ module Extensions
4
+ module FieldSelection
5
+ class ActiveRecordQuerySelector
6
+ attr_reader :selector, :ds, :top_model, :resolved, :root
7
+ # Gets a dataset, a selector...and should return a dataset with the selector definition applied.
8
+ def initialize(ds:, model:, selectors:, resolved:)
9
+ @selector = selectors
10
+ @ds = ds
11
+ @top_model = model
12
+ @resolved = resolved
13
+ @seen = Set.new
14
+ @root = model.table_name
15
+ end
16
+
17
+ def add_select(ds:, model:, table_name:)
18
+ if (fields = fields_for(model))
19
+ # Note, let's always add the pk fields so that associations can load properly
20
+ fields = fields | [model.primary_key.to_sym]
21
+ ds.select(*fields)
22
+ else
23
+ ds
24
+ end
25
+ end
26
+
27
+ def generate
28
+ # TODO: unfortunately, I think we can only control the select clauses for the top model
29
+ # (as I'm not sure ActiveRecord supports expressing it in the join...)
30
+ @ds = add_select(ds: ds, model: top_model, table_name: root)
31
+
32
+ @ds.includes(_eager(top_model, resolved) )
33
+ end
34
+
35
+ def _eager(model, resolved)
36
+ # Cannot select fields in included rels...boooo :()
37
+ # d = add_select(ds: dset, model: model, table_name: model.table_name)
38
+ tracks = only_assoc_for(model, resolved)
39
+ tracks.inject([]) do |dataset, track|
40
+ next dataset if @seen.include?([model, track])
41
+ @seen << [model, track]
42
+ assoc_model = model.associations[track][:model]
43
+ dataset << { track => _eager(assoc_model, resolved[track]) }
44
+ end
45
+ end
46
+
47
+ def only_assoc_for(model, hash)
48
+ hash.keys.reject { |assoc| model.associations[assoc].nil? }
49
+ end
50
+
51
+ def fields_for(model)
52
+ selector[model][:select].to_a
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+ module Praxis
3
+ module Extensions
4
+ module FieldSelection
5
+ class SequelQuerySelector
6
+ attr_reader :selector, :ds, :top_model, :resolved, :root
7
+ # Gets a dataset, a selector...and should return a dataset with the selector definition applied.
8
+ def initialize(ds:, model:, selectors:, resolved:)
9
+ @selector = selectors
10
+ @ds = ds
11
+ @top_model = model
12
+ @resolved = resolved
13
+ @seen = Set.new
14
+ @root = model.table_name
15
+ end
16
+
17
+ def add_select(ds:, model:, table_name:)
18
+ if (fields = fields_for(model))
19
+ # Note, let's always add the pk fields so that associations can load properly
20
+ fields = fields | model.primary_key | [:id]
21
+ qualified = fields.map { |f| Sequel.qualify(table_name, f) }
22
+ ds.select(*qualified)
23
+ else
24
+ ds
25
+ end
26
+ end
27
+
28
+ def generate
29
+ @ds = add_select(ds: ds, model: top_model, table_name: root)
30
+
31
+ tracks = only_assoc_for(top_model, resolved)
32
+ @ds = tracks.inject(@ds) do |dataset, track|
33
+ next dataset if @seen.include?([top_model, track])
34
+ @seen << [top_model, track]
35
+ assoc_model = top_model.associations[track][:model]
36
+ # hash[track] = _eager(assoc_model, resolved[track])
37
+ dataset.eager(track => _eager(assoc_model, resolved[track]))
38
+ end
39
+ end
40
+
41
+ def _eager(model, resolved)
42
+ lambda do |dset|
43
+ d = add_select(ds: dset, model: model, table_name: model.table_name)
44
+
45
+ tracks = only_assoc_for(model, resolved)
46
+ tracks.inject(d) do |dataset, track|
47
+ next dataset if @seen.include?([model, track])
48
+ @seen << [model, track]
49
+ assoc_model = model.associations[track][:model]
50
+ dataset.eager(track => _eager(assoc_model, resolved[track]))
51
+ end
52
+ end
53
+ end
54
+
55
+ def only_assoc_for(model, hash)
56
+ hash.keys.reject { |assoc| model.associations[assoc].nil? }
57
+ end
58
+
59
+ def fields_for(model)
60
+ selector[model][:select].to_a
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,2 @@
1
+ require 'praxis/extensions/rails_compat/request_methods'
2
+ require 'praxis/plugins/rails_plugin'
@@ -0,0 +1,19 @@
1
+ # Make Praxis' request derive from ActionDispatch
2
+ if defined? Praxis::Request
3
+ puts "IT seems that we're trying to redefine Praxis' request parent too late."
4
+ puts "-> try to include the Rails compat pieces earlier in the bootstrap process (before Praxis::Request is requried)"
5
+ exit(-1)
6
+ end
7
+
8
+ begin
9
+ require 'praxis/request_superclassing'
10
+
11
+ module Praxis
12
+ require 'action_dispatch'
13
+ Praxis.request_superclass = ::ActionDispatch::Request
14
+ end
15
+ require 'praxis/request'
16
+ end
17
+
18
+
19
+
@@ -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.instance.validation_handler.handle!(
27
+ Praxis::Application.current_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
- "Are you missing a call Praxis::Application.instance.setup?"
10
+ "Have you forgot to call 'setup' on the Praxis application instance?"
11
11
  end
12
12
 
13
13
 
@@ -2,19 +2,39 @@ module Praxis
2
2
  class MiddlewareApp
3
3
 
4
4
  attr_reader :target
5
-
6
5
  # Initialize the application instance with the desired args, and return the wrapping class.
7
6
  def self.for( **args )
8
- Praxis::Application.instance.setup(**args)
9
- self
10
- end
7
+ 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
+ @args = args
15
+ @app_instance = nil
16
+
17
+ def self.name
18
+ 'MiddlewareApp'
19
+ end
20
+ def self.args
21
+ @args
22
+ end
23
+ def self.setup
24
+ app_instance.setup(**args)
25
+ end
26
+ end
27
+ end
11
28
 
12
29
  def initialize( inner )
13
30
  @target = inner
31
+ self.class.app_instance = Praxis::Application.new(name: self.class.app_name, skip_registration: self.class.skip_registration)
14
32
  end
15
-
33
+
16
34
  def call(env)
17
- result = Praxis::Application.instance.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)
18
38
 
19
39
  unless ( [404,405].include?(result[0].to_i) && result[1]['X-Cascade'] == 'pass' )
20
40
  # Respect X-Cascade header if it doesn't specify 'pass'
@@ -22,6 +22,7 @@ module Praxis
22
22
  TERMINAL_CRLF = /\r\n$/.freeze
23
23
 
24
24
 
25
+ PARAMS_BUF_SIZE = 65536 # Same as implicitly in rack 1.x
25
26
  BUFSIZE = 16384
26
27
 
27
28
  def self.parse(headers,body)
@@ -87,9 +88,9 @@ module Praxis
87
88
 
88
89
  @buf = ""
89
90
 
90
- @params = Rack::Utils::KeySpaceConstrainedParams.new
91
+ @params = new_params
91
92
 
92
- @boundary_size = Rack::Utils.bytesize(@boundary) + EOL.size
93
+ @boundary_size = @boundary.bytesize + EOL.size
93
94
 
94
95
  if @content_length = @headers['Content-Length']
95
96
  @content_length = @content_length.to_i
@@ -98,6 +99,17 @@ module Praxis
98
99
  true
99
100
  end
100
101
 
102
+ if Rack.const_defined?(:RELEASE) && Rack::RELEASE[0] == '2'
103
+ # Rack 2 requires the buffer size
104
+ def new_params
105
+ Rack::Utils::KeySpaceConstrainedParams.new(PARAMS_BUF_SIZE)
106
+ end
107
+ else
108
+ def new_params
109
+ Rack::Utils::KeySpaceConstrainedParams.new
110
+ end
111
+ end
112
+
101
113
  def full_boundary
102
114
  @boundary + EOL
103
115
  end
@@ -10,6 +10,7 @@ module Praxis
10
10
  attr_accessor :headers_attribute
11
11
  attr_accessor :filename_attribute
12
12
  attr_accessor :default_handler
13
+ attr_accessor :application
13
14
 
14
15
  def self.check_option!(name, definition)
15
16
  case name
@@ -77,7 +78,8 @@ module Praxis
77
78
  @name = name
78
79
  @body = body
79
80
  @headers = headers
80
- @default_handler = Praxis::Application.instance.handlers['json']
81
+ @application = Praxis::Application.current_instance
82
+ @default_handler = application.handlers['json']
81
83
 
82
84
  if content_type.nil?
83
85
  self.content_type = 'text/plain'
@@ -212,7 +214,7 @@ module Praxis
212
214
  end
213
215
 
214
216
  def handler
215
- handlers = Praxis::Application.instance.handlers
217
+ handlers = application.handlers
216
218
  (content_type && handlers[content_type.handler_name]) || @default_handler
217
219
  end
218
220
 
@@ -249,7 +251,7 @@ module Praxis
249
251
 
250
252
  # and return that one if it already corresponds to a registered handler
251
253
  # otherwise, add the encoding
252
- if Praxis::Application.instance.handlers.include?(pick.handler_name)
254
+ if application.handlers.include?(pick.handler_name)
253
255
  return pick
254
256
  else
255
257
  return pick + handler_name
@@ -33,7 +33,7 @@ require 'terminal-table'
33
33
  # }
34
34
  # }
35
35
  # 2. log_stats: A String indicating what kind of DB stats you would like
36
- # output into the Praxis::Application.instance.logger app log. Possible
36
+ # output into the Praxis::Application.current_instance.logger app log. Possible
37
37
  # values are: "detailed", "short", and "skip" (i.e. do not print the stats
38
38
  # at all).
39
39
  # 3. stats_log_level: the logging level with which the statistics should be logged.
@@ -238,7 +238,7 @@ module Praxis
238
238
  end
239
239
 
240
240
  def self.to_logger(message)
241
- Praxis::Application.instance.logger.__send__(Plugin.instance.config.stats_log_level, "Praxis::Mapper Statistics: #{message}")
241
+ Praxis::Application.current_instance.logger.__send__(Plugin.instance.config.stats_log_level, "Praxis::Mapper Statistics: #{message}")
242
242
  end
243
243
  end
244
244
  end
@@ -0,0 +1,104 @@
1
+ require 'praxis/plugin'
2
+ require 'praxis/plugin_concern'
3
+
4
+ module Praxis
5
+ module Plugins
6
+ module RailsPlugin
7
+ include Praxis::PluginConcern
8
+
9
+ class Plugin < Praxis::Plugin
10
+
11
+ def setup!
12
+ require 'praxis/dispatcher'
13
+ enable_action_controller_instrumentation
14
+ end
15
+
16
+ private
17
+ def enable_action_controller_instrumentation
18
+ Praxis::Dispatcher.class_eval do
19
+ # Wrap the original action dispatch with a method that instruments rails-expected bits...
20
+ alias_method :orig_instrumented_dispatch, :instrumented_dispatch
21
+
22
+ def instrumented_dispatch( praxis_payload )
23
+ rails_payload = {
24
+ :controller => controller.class.name,
25
+ :action => action.name,
26
+ :params => ( (request.params) ? request.params.dump : {} ),
27
+ :method => request.verb,
28
+ :path => (request.fullpath rescue "unknown")
29
+ }
30
+ Praxis::Notifications.instrument("start_processing.action_controller", rails_payload.dup)
31
+
32
+ Praxis::Notifications.instrument 'process_action.action_controller' do |data|
33
+ begin
34
+ res = orig_instrumented_dispatch(praxis_payload)
35
+ # TODO: also add the db_runtime and view_runtime values...
36
+ data[:status] = res[0]
37
+ res
38
+ ensure
39
+ # Append DB runtime to payload
40
+ #data[:db_runtime] = 999
41
+ # Append rendering time to payload
42
+ #data[:view_runtime] = 123
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ module Request
51
+ end
52
+
53
+ module Controller
54
+ extend ActiveSupport::Concern
55
+
56
+ # Throw in some basic and expected controller methods
57
+
58
+ # Expose a rails-version of params from the controller
59
+ # Avoid using them explicitly in your controllers though. Use request.params object instead, as they are
60
+ # the Praxis ones that have been validated and coerced into the types you've defined.
61
+ def params
62
+ self.request.parameters
63
+ end
64
+
65
+ # Allow accessing the response headers from the controller
66
+ def headers
67
+ self.response.headers
68
+ end
69
+
70
+ def session
71
+ self.request.session
72
+ end
73
+
74
+ # Allow setting the status and body of the response from the controller itself.
75
+ def status=(code)
76
+ self.response.status = code
77
+ end
78
+
79
+ def response_body=(body)
80
+ #TODO: @_rendered = true # Necessary to know if to stop filter chain or not...
81
+ self.response.body = body
82
+ end
83
+
84
+ def head(status, options = {})
85
+ options, status = status, nil if status.is_a?(Hash)
86
+ status ||= options.delete(:status) || :ok
87
+ location = options.delete(:location)
88
+ content_type = options.delete(:content_type)
89
+
90
+ code = Rack::Utils::SYMBOL_TO_STATUS_CODE[status]
91
+ response = Praxis::Response.new(status: code, body: status.to_s, location: location)
92
+
93
+ options.each do |key, value|
94
+ response.headers[key.to_s.dasherize.split('-').each { |v| v[0] = v[0].chr.upcase }.join('-')] = value.to_s
95
+ end
96
+ response.content_type = content_type if content_type
97
+ response
98
+ end
99
+
100
+ end
101
+
102
+ end
103
+ end
104
+ end