praxis 2.0.pre.5 → 2.0.pre.6

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 (76) hide show
  1. checksums.yaml +5 -5
  2. data/.rspec +0 -1
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +22 -0
  5. data/Gemfile +1 -1
  6. data/Guardfile +2 -1
  7. data/Rakefile +1 -7
  8. data/TODO.md +28 -0
  9. data/lib/api_browser/package-lock.json +7110 -0
  10. data/lib/praxis.rb +6 -4
  11. data/lib/praxis/action_definition.rb +9 -16
  12. data/lib/praxis/application.rb +1 -2
  13. data/lib/praxis/bootloader_stages/routing.rb +2 -4
  14. data/lib/praxis/extensions/attribute_filtering.rb +2 -0
  15. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +148 -157
  16. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +15 -0
  17. data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +90 -0
  18. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +68 -0
  19. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +58 -0
  20. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +35 -0
  21. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +9 -12
  22. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +3 -2
  23. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +7 -9
  24. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +6 -9
  25. data/lib/praxis/extensions/pagination.rb +130 -0
  26. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +42 -0
  27. data/lib/praxis/extensions/pagination/header_generator.rb +70 -0
  28. data/lib/praxis/extensions/pagination/ordering_params.rb +234 -0
  29. data/lib/praxis/extensions/pagination/pagination_handler.rb +68 -0
  30. data/lib/praxis/extensions/pagination/pagination_params.rb +374 -0
  31. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +45 -0
  32. data/lib/praxis/handlers/json.rb +2 -0
  33. data/lib/praxis/handlers/www_form.rb +5 -0
  34. data/lib/praxis/handlers/{xml.rb → xml-sample.rb} +6 -0
  35. data/lib/praxis/mapper/active_model_compat.rb +23 -5
  36. data/lib/praxis/mapper/resource.rb +16 -9
  37. data/lib/praxis/mapper/sequel_compat.rb +1 -0
  38. data/lib/praxis/media_type.rb +1 -56
  39. data/lib/praxis/plugins/mapper_plugin.rb +1 -1
  40. data/lib/praxis/plugins/pagination_plugin.rb +71 -0
  41. data/lib/praxis/resource_definition.rb +4 -12
  42. data/lib/praxis/route.rb +2 -4
  43. data/lib/praxis/routing_config.rb +4 -8
  44. data/lib/praxis/tasks/routes.rb +9 -14
  45. data/lib/praxis/validation_handler.rb +1 -2
  46. data/lib/praxis/version.rb +1 -1
  47. data/praxis.gemspec +2 -3
  48. data/spec/functional_spec.rb +9 -6
  49. data/spec/praxis/action_definition_spec.rb +4 -16
  50. data/spec/praxis/api_general_info_spec.rb +6 -6
  51. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +304 -0
  52. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +39 -0
  53. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +34 -0
  54. data/spec/praxis/extensions/field_expansion_spec.rb +6 -24
  55. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +15 -11
  56. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +4 -3
  57. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +130 -0
  58. data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_active_model.rb +45 -2
  59. data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_sequel.rb +0 -0
  60. data/spec/praxis/media_type_spec.rb +5 -129
  61. data/spec/praxis/request_spec.rb +3 -22
  62. data/spec/praxis/resource_definition_spec.rb +1 -1
  63. data/spec/praxis/response_definition_spec.rb +1 -5
  64. data/spec/praxis/route_spec.rb +2 -9
  65. data/spec/praxis/routing_config_spec.rb +4 -13
  66. data/spec/praxis/types/multipart_array_spec.rb +4 -21
  67. data/spec/spec_app/config/environment.rb +0 -2
  68. data/spec/spec_app/design/api.rb +1 -1
  69. data/spec/spec_app/design/media_types/instance.rb +0 -8
  70. data/spec/spec_app/design/media_types/volume.rb +0 -12
  71. data/spec/spec_app/design/resources/instances.rb +1 -2
  72. data/spec/spec_helper.rb +6 -0
  73. data/spec/support/spec_media_types.rb +0 -73
  74. metadata +35 -45
  75. data/spec/praxis/handlers/xml_spec.rb +0 -177
  76. data/spec/praxis/links_spec.rb +0 -68
@@ -0,0 +1,45 @@
1
+ require_relative 'pagination_handler'
2
+
3
+ module Praxis
4
+ module Extensions
5
+ module Pagination
6
+ class SequelPaginationHandler < PaginationHandler
7
+
8
+ def self.where_lt(query, attr, value)
9
+ query.where("#{attr} < ?", value)
10
+ end
11
+
12
+ def self.where_gt(query, attr, value)
13
+ query.where("#{attr} > ?", value)
14
+ end
15
+
16
+ def self.order(query, order)
17
+ return query unless order
18
+ order_clause = order.map do |spec_hash|
19
+ direction, name = spec_hash.first
20
+ case direction.to_sym
21
+ when :desc
22
+ Sequel.desc(name.to_sym)
23
+ else
24
+ Sequel.asc(name.to_sym)
25
+ end
26
+ end
27
+ query = query.order(*order_clause)
28
+ query
29
+ end
30
+
31
+ def self.count(query)
32
+ query.count
33
+ end
34
+
35
+ def self.offset(query, offset)
36
+ query.offset(offset)
37
+ end
38
+
39
+ def self.limit(query, limit)
40
+ query.limit(limit)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -17,6 +17,8 @@ module Praxis
17
17
  # @param [String] document
18
18
  # @return [Hash,Array] the structured-data representation of the document
19
19
  def parse(document)
20
+ # Try to be nice and accept an empty string as an empty payload (seems nice to do for dumb http clients)
21
+ return nil if (document.nil? || document == '')
20
22
  ::JSON.parse(document)
21
23
  end
22
24
 
@@ -1,3 +1,8 @@
1
+ # This is an example of a handler that can load and generate www-url-encoded payloads.
2
+ # Note that if you use your API to pass nil values for attributes as a way to unset their
3
+ # values, this handler will not work (as there isn't necessarily a defined "null" value in
4
+ # this encoding (although you can probably define how to encode/decode it and use it as such)
5
+ # Use at your own risk.
1
6
  module Praxis
2
7
  module Handlers
3
8
  class WWWForm
@@ -1,3 +1,9 @@
1
+ # This is an example of a handler that can load and generate 'activesupport-style' xml payloads.
2
+ # Note that if you use your API to pass nil values for attributes as a way to unset their values,
3
+ # this handler will not work (as there isn't necessarily a defined "null" value in this encoding
4
+ # (although you can probably define how to encode/decode it and use it as such)
5
+ # Use at your own risk
6
+
1
7
  module Praxis
2
8
  module Handlers
3
9
  class XML
@@ -3,6 +3,7 @@
3
3
  require 'active_support/concern'
4
4
 
5
5
  require 'praxis/extensions/field_selection/active_record_query_selector'
6
+ require 'praxis/extensions/attribute_filtering/active_record_filter_query_builder'
6
7
 
7
8
  module Praxis
8
9
  module Mapper
@@ -15,7 +16,7 @@ module Praxis
15
16
 
16
17
  module ClassMethods
17
18
  def _filter_query_builder_class
18
- Praxis::Extensions::ActiveRecordFilterQueryBuilder
19
+ Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
19
20
  end
20
21
 
21
22
  def _field_selector_query_builder_class
@@ -57,19 +58,36 @@ module Praxis
57
58
  end
58
59
  end
59
60
 
61
+ def _join_foreign_key_for(assoc_reflection)
62
+ maj, min, _ = ActiveRecord.gem_version.segments
63
+ if maj >= 6 && min >=1
64
+ assoc_reflection.join_foreign_key.to_sym
65
+ else
66
+ assoc_reflection.join_keys.foreign_key.to_sym
67
+ end
68
+ end
69
+
70
+ def _join_primary_key_for(assoc_reflection)
71
+ maj, min, _ = ActiveRecord.gem_version.segments
72
+ if maj >= 6 && min >=1
73
+ assoc_reflection.join_primary_key.to_sym
74
+ else
75
+ assoc_reflection.join_keys.key.to_sym
76
+ end
77
+ end
60
78
  private
61
79
  def local_columns_used_for_the_association(type, assoc_reflection)
62
80
  case type
63
81
  when :one_to_many
64
82
  # The associated table will point to us by key (usually the PK, but not always)
65
- [assoc_reflection.join_keys.foreign_key.to_sym]
83
+ [_join_foreign_key_for(assoc_reflection)]
66
84
  when :many_to_one
67
85
  # We have the FKs to the associated model
68
- [assoc_reflection.join_keys.foreign_key.to_sym]
86
+ [_join_foreign_key_for(assoc_reflection)]
69
87
  when :many_to_many
70
88
  ref = resolve_closest_through_reflection(assoc_reflection)
71
89
  # The associated middle table will point to us by key (usually the PK, but not always)
72
- [ref.join_keys.foreign_key.to_sym] # The foreign key that the last through table points to
90
+ [_join_foreign_key_for(ref)] # The foreign key that the last through table points to
73
91
  else
74
92
  raise "association type #{type} not supported"
75
93
  end
@@ -80,7 +98,7 @@ module Praxis
80
98
  # will always get us the right column
81
99
  case type
82
100
  when :one_to_many, :many_to_one, :many_to_many
83
- [assoc_reflection.join_keys.key.to_sym]
101
+ [_join_primary_key_for(assoc_reflection)]
84
102
  else
85
103
  raise "association type #{type} not supported"
86
104
  end
@@ -196,17 +196,24 @@ module Praxis::Mapper
196
196
  end
197
197
 
198
198
  # TODO: this shouldn't be needed if we incorporate it with the properties of the mapper...
199
- def self.filters_mapping(hash)
200
- @_filter_query_builder_class = model._filter_query_builder_class.for(**hash)
201
- end
202
-
203
- def self._filter_query_builder_class
204
- @_filter_query_builder_class
199
+ # ...maybe what this means is that we can change it for a better DSL in the resource?
200
+ def self.filters_mapping(definition)
201
+ @_filters_map = \
202
+ case definition
203
+ when Hash
204
+ definition
205
+ when Array
206
+ definition.each_with_object({}) { |item, hash| hash[item.to_sym] = item }
207
+ else
208
+ raise "Resource.filters_mapping only allows a hash or an array"
209
+ end
205
210
  end
206
211
 
207
212
  def self.craft_filter_query(base_query, filters:) # rubocop:disable Metrics/AbcSize
208
- if filters && _filter_query_builder_class
209
- base_query = _filter_query_builder_class.new(query: base_query, model: model).build_clause(filters)
213
+ if filters
214
+ raise "Must define the mapping of filters if want to use Filtering for resource: #{self}" unless @_filters_map
215
+ debug = Praxis::Application.instance.config.mapper.debug_queries
216
+ base_query = model._filter_query_builder_class.new(query: base_query, model: model, filters_map: @_filters_map, debug: debug).generate(filters)
210
217
  end
211
218
 
212
219
  base_query
@@ -215,7 +222,7 @@ module Praxis::Mapper
215
222
  def self.craft_field_selection_query(base_query, selectors:) # rubocop:disable Metrics/AbcSize
216
223
  if selectors && model._field_selector_query_builder_class
217
224
  debug = Praxis::Application.instance.config.mapper.debug_queries
218
- base_query = model._field_selector_query_builder_class.new(query: base_query, selectors: selectors).generate(debug: debug)
225
+ base_query = model._field_selector_query_builder_class.new(query: base_query, selectors: selectors, debug: debug).generate
219
226
  end
220
227
 
221
228
  base_query
@@ -11,6 +11,7 @@ module Praxis::Mapper
11
11
 
12
12
  module ClassMethods
13
13
  def _filter_query_builder_class
14
+ # TODO: refactor the query builder, and add the explicit require in this file
14
15
  Praxis::Extensions::SequelFilterQueryBuilder
15
16
  end
16
17
 
@@ -7,15 +7,12 @@ module Praxis
7
7
  # encodings; for example, a controller might respond with an actual Widget object, but a
8
8
  # Content-Type header specifying 'application/vnd.acme.widget+json'; Praxis uses the information
9
9
  # contained in the media-type definition of Widget to transform the object into an equivalent
10
- # JSON representation. If the content type ends with '+xml' instead, and the XML handler is
11
- # registered with the framework, Praxis will respond with an XML representation of the
12
- # widget. The use of media types allows your application's models to be decoupled from its
10
+ # JSON representation. The use of media types allows your application's models to be decoupled from its
13
11
  # HTTP interface specification.
14
12
  #
15
13
  # A media type definition consists of:
16
14
  # - a MIME type identifier
17
15
  # - attributes, each of which has a name and a data type
18
- # - named links to other resources
19
16
  # - named views, which expose interesting subsets of attributes
20
17
  #
21
18
  # @example Declare a widget type that's used by my supply-chain management app
@@ -37,16 +34,6 @@ module Praxis
37
34
  # description: 'The factory in which this widget was produced'
38
35
  # end
39
36
  #
40
- # links do
41
- # link :factory,
42
- # description: 'Link to the factory in which this widget was produced'
43
- # end
44
- #
45
- # # If widgets can be linked-to by other resources, they should have a link view
46
- # view :link do
47
- # attribute :href
48
- # end
49
- #
50
37
  # # All resources should have a default view
51
38
  # view :default do
52
39
  # attribute :id
@@ -58,32 +45,6 @@ module Praxis
58
45
 
59
46
  include Types::MediaTypeCommon
60
47
 
61
- class DSLCompiler < Attributor::HashDSLCompiler
62
- def links(&block)
63
- attribute :links, Praxis::Links.for(options[:reference]), dsl_compiler: Links::DSLCompiler, &block
64
- end
65
- end
66
-
67
- def self.attributes(opts={}, &block)
68
- super(opts.merge(dsl_compiler: MediaType::DSLCompiler), &block)
69
- end
70
-
71
- def self._finalize!
72
- super
73
-
74
- # Only define our special links accessor if it was setup using the special DSL
75
- # (we might have an app defining an attribute called `links` on its own, in which
76
- # case we leave it be)
77
- if @attribute && self.attributes.key?(:links) && self.attributes[:links].type < Praxis::Links
78
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
79
- def links
80
- self.class::Links.new(@object)
81
- end
82
- RUBY
83
- end
84
- end
85
-
86
-
87
48
  class FieldResolver
88
49
  def self.resolve(type,fields)
89
50
  self.new.resolve(type,fields)
@@ -118,30 +79,14 @@ module Praxis
118
79
 
119
80
 
120
81
  fields.each do |name, sub_fields|
121
- # skip links and do them below
122
- next if name == :links && defined?(type::Links)
123
82
 
124
83
  new_type = type.attributes[name].type
125
84
  result[name] = resolve(new_type, sub_fields)
126
85
  end
127
86
 
128
- # now to tackle whatever links there may be
129
- if defined?(type::Links) &&(links_fields = fields[:links])
130
- resolved_links = resolve_links(type::Links, links_fields)
131
- self.deep_merge(result, resolved_links)
132
- end
133
-
134
87
  result
135
88
  end
136
89
 
137
- def resolve_links(links_type, links)
138
- links.each_with_object({}) do |(name, link_fields), hash|
139
- using = links_type.links[name]
140
- new_type = links_type.attributes[name].type
141
- hash[using] = resolve(new_type, link_fields)
142
- end
143
- end
144
-
145
90
  # perform a deep recursive *in place* merge
146
91
  # form all values in +source+ onto +target+
147
92
  #
@@ -1,5 +1,5 @@
1
1
  require 'singleton'
2
- require 'praxis/extensions/attribute_filtering/filtering_params'
2
+ require 'praxis/extensions/attribute_filtering'
3
3
 
4
4
  module Praxis
5
5
  module Plugins
@@ -0,0 +1,71 @@
1
+ require 'singleton'
2
+ require 'praxis/extensions/pagination'
3
+
4
+ # Simple plugin concept
5
+ # Example configuration for this plugin
6
+ # Praxis::Application.configure do |application|
7
+ # application.bootloader.use Praxis::Plugins::PaginationPlugin, {
8
+ # max_items: 500, # Unlimited by default,
9
+ # default_page_size: 100,
10
+ # disallow_paging_by_default: false,
11
+ # # See all available options below
12
+ # end
13
+ # end
14
+ #
15
+ # This would be applied to all controllers etc...so if one does not that
16
+ # It can easily add the `include Praxis::Extensions::Pagination` for every controller
17
+ # and use the class `Praxis::Types::PaginationParams.xxx yyy` stanzas to configure defaults
18
+
19
+ module Praxis
20
+ module Plugins
21
+ module PaginationPlugin
22
+ include Praxis::PluginConcern
23
+
24
+ class Plugin < Praxis::Plugin
25
+ include Singleton
26
+
27
+ def config_key
28
+ :pagination
29
+ end
30
+
31
+ def load_config!
32
+ @options || {}
33
+ end
34
+
35
+ def prepare_config!(node)
36
+ node.attributes do
37
+ attribute :max_items, Integer # Defaults to unlimited
38
+ attribute :default_page_size, Integer, default: Praxis::Types::PaginationParams.default_page_size
39
+ attribute :paging_default_mode, Hash, default: Praxis::Types::PaginationParams.paging_default_mode
40
+ attribute :disallow_paging_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_paging_by_default
41
+ attribute :disallow_cursor_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_cursor_by_default
42
+ attribute :disallow_cursor_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_cursor_by_default
43
+ attribute :sorting do
44
+ attribute :enforce_all_fields, Attributor::Boolean, default: Praxis::Types::OrderingParams.enforce_all_fields
45
+ end
46
+ end
47
+ end
48
+
49
+ def setup!
50
+ self.config.each do |name, val|
51
+ if name == :sorting
52
+ val.each do |ordername, orderval|
53
+ Praxis::Types::OrderingParams.send(ordername, orderval)
54
+ end
55
+ else
56
+ Praxis::Types::PaginationParams.send(name, val)
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ module Controller
63
+ extend ActiveSupport::Concern
64
+
65
+ included do
66
+ include Praxis::Extensions::Pagination
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -95,14 +95,6 @@ module Praxis
95
95
  @on_finalize
96
96
  end
97
97
 
98
- # FIXME: this is inconsistent with the rest of the magic DSL convention.
99
- def routing(&block)
100
- warn "DEPRECATED: ResourceDefinition.routing is deprecated use prefix directly instead."
101
-
102
- # eval this assuming it will only call #prefix
103
- self.instance_eval(&block)
104
- end
105
-
106
98
  def prefix(prefix=nil)
107
99
  return @prefix if prefix.nil?
108
100
  @routing_prefix = nil # reset routing_prefix
@@ -125,7 +117,7 @@ module Praxis
125
117
  @routing_prefix = nil # reset routing_prefix
126
118
 
127
119
  parent_action = parent.canonical_path
128
- parent_route = parent_action.primary_route.path
120
+ parent_route = parent_action.route.path
129
121
 
130
122
  # if a mapping is passed, it *must* resolve any param name conflicts
131
123
  unless mapping.any?
@@ -146,7 +138,7 @@ module Praxis
146
138
  if mapping.key?(name)
147
139
  param = mapping[name]
148
140
  # FIXME: this won't handle URI Template type paths, ie '/{parent_id}'
149
- prefixed_path = parent_action.primary_route.prefixed_path
141
+ prefixed_path = parent_action.route.prefixed_path
150
142
  @parent_prefix = prefixed_path.gsub(/(:)(#{name})(\W+|$)/, "\\1#{param.to_s}\\3")
151
143
  else
152
144
  mapping[name] = name
@@ -221,14 +213,14 @@ module Praxis
221
213
  end
222
214
 
223
215
  def to_href( params )
224
- canonical_path.primary_route.path.expand(params.transform_values(&:to_s))
216
+ canonical_path.route.path.expand(params)
225
217
  end
226
218
 
227
219
  def parse_href(path)
228
220
  if path.kind_of?(::URI::Generic)
229
221
  path = path.path
230
222
  end
231
- param_values = canonical_path.primary_route.path.params(path)
223
+ param_values = canonical_path.route.path.params(path)
232
224
  attrs = canonical_path.params.attributes
233
225
  param_values.each_with_object({}) do |(key,value),hash|
234
226
  hash[key.to_sym] = attrs[key.to_sym].load(value,[key])
@@ -1,13 +1,12 @@
1
1
  module Praxis
2
2
 
3
3
  class Route
4
- attr_accessor :verb, :path, :version, :name, :prefixed_path, :options
4
+ attr_accessor :verb, :path, :version, :prefixed_path, :options
5
5
 
6
- def initialize(verb, path, version='n/a', name:nil, prefixed_path:nil, **options)
6
+ def initialize(verb, path, version='n/a', prefixed_path:nil, **options)
7
7
  @verb = verb
8
8
  @path = path
9
9
  @version = version
10
- @name = name
11
10
  @options = options
12
11
  @prefixed_path = prefixed_path
13
12
  end
@@ -34,7 +33,6 @@ module Praxis
34
33
  path: path.to_s,
35
34
  version: version
36
35
  }
37
- result[:name] = name unless name.nil?
38
36
  result[:options] = options if options.any?
39
37
  result
40
38
  end
@@ -1,7 +1,7 @@
1
1
  module Praxis
2
2
  class RoutingConfig
3
3
 
4
- attr_reader :routes
4
+ attr_reader :route
5
5
  attr_reader :version
6
6
  attr_reader :base
7
7
 
@@ -10,7 +10,7 @@ module Praxis
10
10
  @base = base
11
11
  @prefix_segments = Array(prefix)
12
12
 
13
- @routes = []
13
+ @route = nil
14
14
 
15
15
  if block_given?
16
16
  instance_eval(&block)
@@ -53,12 +53,8 @@ module Praxis
53
53
  end
54
54
  prefixed_path = path.gsub('//','/')
55
55
  path = (base + path).gsub('//','/')
56
- # Reject our own options
57
- route_name = options.delete(:name);
58
- pattern = Mustermann.new(path, **{ignore_unknown_options: true}.merge( options ))
59
- route = Route.new(verb, pattern, version, name: route_name, prefixed_path: prefixed_path, **options)
60
- @routes << route
61
- route
56
+ pattern = Mustermann.new(path, {ignore_unknown_options: true}.merge( options ))
57
+ @route = Route.new(verb, pattern, version, prefixed_path: prefixed_path, **options)
62
58
  end
63
59
 
64
60
  end