praxis 0.21 → 0.22.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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