praxis 0.21 → 2.0.pre.3

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 (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