praxis 0.21 → 2.0.pre.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -15
  3. data/CHANGELOG.md +328 -299
  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 +9 -3
  28. data/lib/praxis/action_definition.rb +1 -1
  29. data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
  30. data/lib/praxis/application.rb +1 -9
  31. data/lib/praxis/bootloader.rb +1 -4
  32. data/lib/praxis/config.rb +1 -1
  33. data/lib/praxis/dispatcher.rb +10 -6
  34. data/lib/praxis/docs/generator.rb +2 -1
  35. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +180 -0
  36. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +273 -0
  37. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
  38. data/lib/praxis/extensions/field_selection.rb +1 -9
  39. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +51 -0
  40. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +61 -0
  41. data/lib/praxis/extensions/rails_compat.rb +2 -0
  42. data/lib/praxis/extensions/rails_compat/request_methods.rb +19 -0
  43. data/lib/praxis/handlers/xml.rb +1 -1
  44. data/lib/praxis/mapper/active_model_compat.rb +98 -0
  45. data/lib/praxis/mapper/resource.rb +242 -0
  46. data/lib/praxis/mapper/selector_generator.rb +149 -0
  47. data/lib/praxis/mapper/sequel_compat.rb +76 -0
  48. data/lib/praxis/media_type_identifier.rb +2 -1
  49. data/lib/praxis/middleware_app.rb +20 -2
  50. data/lib/praxis/multipart/parser.rb +14 -2
  51. data/lib/praxis/notifications.rb +1 -1
  52. data/lib/praxis/plugins/mapper_plugin.rb +64 -0
  53. data/lib/praxis/plugins/rails_plugin.rb +104 -0
  54. data/lib/praxis/request.rb +7 -1
  55. data/lib/praxis/request_superclassing.rb +11 -0
  56. data/lib/praxis/resource_definition.rb +5 -5
  57. data/lib/praxis/response.rb +1 -1
  58. data/lib/praxis/route.rb +1 -1
  59. data/lib/praxis/routing_config.rb +1 -1
  60. data/lib/praxis/trait.rb +1 -1
  61. data/lib/praxis/types/media_type_common.rb +2 -2
  62. data/lib/praxis/types/multipart.rb +1 -1
  63. data/lib/praxis/types/multipart_array.rb +2 -2
  64. data/lib/praxis/types/multipart_array/part_definition.rb +1 -1
  65. data/lib/praxis/version.rb +1 -1
  66. data/praxis.gemspec +14 -13
  67. data/spec/functional_spec.rb +4 -7
  68. data/spec/praxis/action_definition_spec.rb +1 -1
  69. data/spec/praxis/application_spec.rb +1 -1
  70. data/spec/praxis/collection_spec.rb +3 -2
  71. data/spec/praxis/config_spec.rb +2 -2
  72. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +106 -0
  73. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +147 -0
  74. data/spec/praxis/extensions/field_selection/support/spec_resources_active_model.rb +130 -0
  75. data/spec/praxis/extensions/field_selection/support/spec_resources_sequel.rb +106 -0
  76. data/spec/praxis/handlers/xml_spec.rb +2 -2
  77. data/spec/praxis/mapper/resource_spec.rb +169 -0
  78. data/spec/praxis/mapper/selector_generator_spec.rb +293 -0
  79. data/spec/praxis/media_type_spec.rb +0 -10
  80. data/spec/praxis/middleware_app_spec.rb +29 -9
  81. data/spec/praxis/request_stages/action_spec.rb +8 -1
  82. data/spec/praxis/response_definition_spec.rb +7 -4
  83. data/spec/praxis/response_spec.rb +1 -1
  84. data/spec/praxis/responses/internal_server_error_spec.rb +2 -2
  85. data/spec/praxis/responses/validation_error_spec.rb +2 -2
  86. data/spec/praxis/router_spec.rb +1 -1
  87. data/spec/spec_app/app/controllers/instances.rb +1 -1
  88. data/spec/spec_app/config/environment.rb +3 -21
  89. data/spec/spec_helper.rb +11 -15
  90. data/spec/support/be_deep_equal_matcher.rb +39 -0
  91. data/spec/support/spec_resources.rb +124 -0
  92. data/tasks/thor/templates/generator/empty_app/Gemfile +3 -3
  93. metadata +102 -77
  94. data/.ruby-version +0 -1
  95. data/lib/praxis/extensions/mapper_selectors.rb +0 -16
  96. data/lib/praxis/media_type_collection.rb +0 -127
  97. data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
  98. data/lib/praxis/stats.rb +0 -113
  99. data/spec/praxis/media_type_collection_spec.rb +0 -157
  100. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
  101. data/spec/praxis/stats_spec.rb +0 -9
  102. data/spec/spec_app/app/models/person.rb +0 -3
@@ -0,0 +1,149 @@
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
+ if resource.properties.key?(name)
23
+ add_property(name, fields)
24
+ elsif resource.model._praxis_associations.key?(name)
25
+ add_association(name, fields)
26
+ else
27
+ add_select(name)
28
+ end
29
+ end
30
+
31
+ def add_association(name, fields)
32
+
33
+ association = resource.model._praxis_associations.fetch(name) do
34
+ raise "missing association for #{resource} with name #{name}"
35
+ end
36
+ associated_resource = resource.model_map[association[:model]]
37
+ unless associated_resource
38
+ raise "Whoops! could not find a resource associated with model #{association[:model]} (root resource #{resource})"
39
+ end
40
+ # Add the required columns in this model to make sure the association can be loaded
41
+ association[:local_key_columns].each {|col| add_select(col) }
42
+
43
+ node = SelectorGeneratorNode.new(associated_resource)
44
+ if association[:remote_key_columns].nil?
45
+ binding.pry
46
+ puts association
47
+ end
48
+ unless association[:remote_key_columns].empty?
49
+ # Make sure we add the required columns for this association to the remote model query
50
+ fields = {} if fields == true
51
+ new_fields_as_hash = association[:remote_key_columns].each_with_object({}) do|name, hash|
52
+ hash[name] = true
53
+ end
54
+ fields.merge!(new_fields_as_hash)
55
+ end
56
+
57
+ node.add(fields) unless fields == true
58
+
59
+ self.merge_track(name, node)
60
+ end
61
+
62
+ def add_select(name)
63
+ return @select_star = true if name == :*
64
+ return if @select_star
65
+ @select.add name
66
+ end
67
+
68
+ def add_property(name, fields)
69
+ dependencies = resource.properties[name][:dependencies]
70
+ # Always add the underlying association if we're overriding the name...
71
+ add_association(name, fields) if resource.model._praxis_associations.key?(name)
72
+ if dependencies
73
+ dependencies.each do |dependency|
74
+ # To detect recursion, let's allow mapping depending fields to the same name of the property
75
+ # but properly detecting if it's a real association...in which case we've already added it above
76
+ if dependency == name
77
+ add_select(name) unless resource.model._praxis_associations.key?(name)
78
+ else
79
+ apply_dependency(dependency)
80
+ end
81
+ end
82
+ end
83
+
84
+ head, *tail = resource.properties[name][:through]
85
+ return if head.nil?
86
+
87
+ new_fields = tail.reverse.inject(fields) do |thing, step|
88
+ {step => thing}
89
+ end
90
+
91
+ add_association(head, new_fields)
92
+ end
93
+
94
+ def apply_dependency(dependency)
95
+ case dependency
96
+ when Symbol
97
+ map_property(dependency, true)
98
+ when String
99
+ head, tail = dependency.split('.').collect(&:to_sym)
100
+ raise "String dependencies can not be singular" if tail.nil?
101
+
102
+ add_association(head, {tail => true})
103
+ end
104
+ end
105
+
106
+ def merge_track( track_name, node )
107
+ raise "Cannot merge another node for association #{track_name}: incompatible model" unless node.model == self.model
108
+
109
+ existing = self.tracks[track_name]
110
+ if existing
111
+ node.select.each do|col_name|
112
+ existing.add_select(col_name)
113
+ end
114
+ node.tracks.each do |name, n|
115
+ existing.merge(name, n)
116
+ end
117
+ else
118
+ self.tracks[track_name] = node
119
+ end
120
+
121
+ end
122
+
123
+ def dump
124
+ hash = {}
125
+ hash[:model] = resource.model
126
+ if !@select.empty? || @select_star
127
+ hash[:columns] = @select_star ? [ :* ] : @select.to_a
128
+ end
129
+ unless @tracks.empty?
130
+ hash[:tracks] = @tracks.each_with_object({}) {|(name, node), hash| hash[name] = node.dump }
131
+ end
132
+ hash
133
+ end
134
+ end
135
+
136
+ # Generates a set of selectors given a resource and
137
+ # list of resource attributes.
138
+ class SelectorGenerator
139
+ # Entry point
140
+ def add(resource, fields)
141
+ @root = SelectorGeneratorNode.new(resource)
142
+ @root.add(fields)
143
+ end
144
+
145
+ def selectors
146
+ @root
147
+ end
148
+ end
149
+ end
@@ -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
 
@@ -5,15 +5,33 @@ module Praxis
5
5
 
6
6
  # Initialize the application instance with the desired args, and return the wrapping class.
7
7
  def self.for( **args )
8
- Praxis::Application.instance.setup(**args)
9
- self
8
+ Class.new(self) do
9
+ @args = args
10
+ @setup_done = false
11
+ def self.name
12
+ 'MiddlewareApp'
13
+ end
14
+ def self.args
15
+ @args
16
+ end
17
+ def self.setup_done
18
+ @setup_done
19
+ end
20
+ def self.setup
21
+ @setup_done = true
22
+ Praxis::Application.instance.setup(**@args)
23
+ end
24
+ end
10
25
  end
11
26
 
12
27
  def initialize( inner )
13
28
  @target = inner
29
+ @setup_done = false
14
30
  end
15
31
 
16
32
  def call(env)
33
+ self.class.setup unless self.class.setup_done
34
+
17
35
  result = Praxis::Application.instance.call(env)
18
36
 
19
37
  unless ( [404,405].include?(result[0].to_i) && result[1]['X-Cascade'] == '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
@@ -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
@@ -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