praxis 2.0.pre.10 → 2.0.pre.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +1 -3
  4. data/CHANGELOG.md +26 -0
  5. data/bin/praxis +65 -2
  6. data/lib/praxis/api_definition.rb +8 -4
  7. data/lib/praxis/bootloader_stages/environment.rb +1 -0
  8. data/lib/praxis/collection.rb +11 -0
  9. data/lib/praxis/docs/open_api/response_object.rb +21 -6
  10. data/lib/praxis/docs/open_api_generator.rb +1 -1
  11. data/lib/praxis/extensions/attribute_filtering.rb +14 -1
  12. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +206 -66
  13. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
  14. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +45 -41
  15. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
  16. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
  17. data/lib/praxis/extensions/pagination.rb +5 -32
  18. data/lib/praxis/mapper/active_model_compat.rb +4 -0
  19. data/lib/praxis/mapper/resource.rb +18 -2
  20. data/lib/praxis/mapper/selector_generator.rb +1 -0
  21. data/lib/praxis/mapper/sequel_compat.rb +7 -0
  22. data/lib/praxis/media_type_identifier.rb +11 -1
  23. data/lib/praxis/plugins/mapper_plugin.rb +22 -13
  24. data/lib/praxis/plugins/pagination_plugin.rb +34 -4
  25. data/lib/praxis/response_definition.rb +46 -66
  26. data/lib/praxis/responses/http.rb +3 -1
  27. data/lib/praxis/tasks/api_docs.rb +4 -1
  28. data/lib/praxis/tasks/routes.rb +6 -6
  29. data/lib/praxis/version.rb +1 -1
  30. data/spec/praxis/action_definition_spec.rb +3 -1
  31. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +267 -167
  32. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
  33. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +100 -17
  34. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
  35. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
  36. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
  37. data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
  38. data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
  39. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  40. data/spec/praxis/response_definition_spec.rb +37 -129
  41. data/tasks/thor/example.rb +12 -6
  42. data/tasks/thor/model.rb +40 -0
  43. data/tasks/thor/scaffold.rb +117 -0
  44. data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
  45. data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
  46. data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
  47. data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
  48. data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
  49. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +15 -0
  50. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
  51. data/tasks/thor/templates/generator/example_app/config.ru +1 -2
  52. data/tasks/thor/templates/generator/example_app/config/environment.rb +3 -2
  53. data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
  54. data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
  55. data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
  56. data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
  57. data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
  58. data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
  59. data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
  60. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
  61. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
  62. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
  63. data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
  64. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
  65. data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
  66. data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
  67. metadata +21 -6
@@ -3,7 +3,8 @@ module Praxis
3
3
  module AttributeFiltering
4
4
  class FilterTreeNode
5
5
  attr_reader :path, :conditions, :children
6
- # # parsed_filters is an Array of {name: X, op: , value: } ... exactly the format of the FilteringParams.load method
6
+ # Parsed_filters is an Array of {name: X, op: Y, value: Z} ... exactly the format of the FilteringParams.load method
7
+ # It can also contain a :node_object
7
8
  def initialize(parsed_filters, path: [])
8
9
  @path = path # Array that marks the tree 'path' to this node (with respect to the absolute root)
9
10
  @conditions = [] # Conditions to apply directly to this node
@@ -14,7 +15,7 @@ module Praxis
14
15
  if components.empty?
15
16
  return
16
17
  elsif components.size == 1
17
- @conditions << hash.slice(:name, :op, :value)
18
+ @conditions << hash.slice(:name, :op, :value, :fuzzy, :node_object)
18
19
  else
19
20
  children_data[components.first] ||= []
20
21
  children_data[components.first] << hash
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
- # rubocop:disable all
2
+ require 'praxis/extensions/attribute_filtering/filters_parser'
3
+
3
4
  #
4
- # Attributor type to define and handlea simple language to express filtering attributes in listings.
5
+ # Attributor type to define and handle the language to express filtering attributes in listings.
5
6
  # Commonly used in a query string parameter value for listing calls.
6
7
  #
7
8
  # The type allows you to restrict the allowable fields (and their types) based on an existing Mediatype.
@@ -14,7 +15,7 @@
14
15
  # attribute :filters,
15
16
  # Types::FilteringParams.for(MediaTypes::MyType) do
16
17
  # filter 'user.id', using: ['=', '!=']
17
- # filter 'name', using: ['=', '!=']
18
+ # filter 'name', using: ['=', '!=', '!', '!!]
18
19
  # filter 'children.created_at', using: ['>', '>=', '<', '<=']
19
20
  # filter 'display_name', using: ['=', '!='], fuzzy: true
20
21
  # end
@@ -26,6 +27,8 @@ module Praxis
26
27
  include Attributor::Type
27
28
  include Attributor::Dumpable
28
29
 
30
+ attr_reader :parsed_array
31
+
29
32
  class DSLCompiler < Attributor::DSLCompiler
30
33
  # "account.id": { operators: ["=", "!="] },
31
34
  # name: { operators: ["=", "!="], fuzzy_match: true },
@@ -36,9 +39,9 @@ module Praxis
36
39
  end
37
40
  end
38
41
 
39
- VALUE_REGEX = /[^,&]*/
40
- AVAILABLE_OPERATORS = Set.new(['!=', '>=', '<=', '=', '<', '>','!','!!']).freeze
41
- FILTER_REGEX = /(?<attribute>([^=!><])+)(?<operator>!=|>=|<=|!!|=|<|>|!)(?<value>#{VALUE_REGEX}(,#{VALUE_REGEX})*)/
42
+ VALUE_OPERATORS = Set.new(['!=', '>=', '<=', '=', '<', '>']).freeze
43
+ NOVALUE_OPERATORS = Set.new(['!','!!']).freeze
44
+ AVAILABLE_OPERATORS = Set.new(VALUE_OPERATORS+NOVALUE_OPERATORS).freeze
42
45
 
43
46
  # Abstract class, which needs to be used by subclassing it through the .for method, to set the allowed filters
44
47
  # definition should be a hash, keyed by field name, which contains a hash that can have two pieces of metadata
@@ -52,7 +55,7 @@ module Praxis
52
55
  def for(media_type, **_opts)
53
56
  unless media_type < Praxis::MediaType
54
57
  raise ArgumentError, "Invalid type: #{media_type.name} for Filters. " \
55
- 'Must be a subclass of MediaType'
58
+ 'Using the .for method for defining a filter, requires passing a subclass of a MediaType'
56
59
  end
57
60
 
58
61
  ::Class.new(self) do
@@ -77,9 +80,7 @@ module Praxis
77
80
  }
78
81
  end
79
82
  end
80
-
81
- attr_reader :parsed_array
82
-
83
+
83
84
  def self.native_type
84
85
  self
85
86
  end
@@ -134,11 +135,15 @@ module Praxis
134
135
  raise "filter with name #{filter_name} does not correspond to an existing field inside " \
135
136
  " MediaType #{media_type.name}"
136
137
  end
137
- attr_example = filter_components.inject(mt_example) do |last, name|
138
- # we can safely do sends, since we've verified the components are valid
139
- last.send(name)
140
- end
141
- arr << "#{filter_name}#{op}#{attr_example}"
138
+ if NOVALUE_OPERATORS.include?(op)
139
+ arr << "#{filter_name}#{op}" # Do not add a value for the operators that don't take it
140
+ else
141
+ attr_example = filter_components.inject(mt_example) do |last, name|
142
+ # we can safely do sends, since we've verified the components are valid
143
+ last.send(name)
144
+ end
145
+ arr << "#{filter_name}#{op}#{attr_example}"
146
+ end
142
147
  end.join('&')
143
148
  else
144
149
  'name=Joe&date>2017-01-01'
@@ -153,34 +158,33 @@ module Praxis
153
158
 
154
159
  def self.load(filters, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
155
160
  return filters if filters.is_a?(native_type)
156
- return new if filters.nil?
157
- parsed = filters.split('&').each_with_object([]) do |filter_string, arr|
158
- match = FILTER_REGEX.match(filter_string)
159
- values = CGI.unescape(match[:value]).split(',')
160
- value = if values.size > 1
161
- multimatch = true
162
- values
163
- else
164
- multimatch = false
165
- values.first
166
- end
167
-
168
- attr_name = match[:attribute].to_sym
169
- coerced = if media_type
170
- filter_components = attr_name.to_s.split('.').map(&:to_sym)
171
- attr, _enclosing_type = find_filter_attribute(filter_components, media_type)
172
- if multimatch
173
- attr_coll = Attributor::Collection.of(attr.type)
174
- attr_coll.load(value)
161
+ return new if filters.nil? || filters.blank?
162
+
163
+ parsed = Parser.new.parse(filters)
164
+
165
+ tree = ConditionGroup.load(parsed)
166
+
167
+ rr = tree.flattened_conditions
168
+ accum = []
169
+ rr.each do |spec|
170
+ attr_name = spec[:name]
171
+ # TODO: Do we need to CGI.unescape things? here or even before??...
172
+ coerced = \
173
+ if media_type
174
+ filter_components = attr_name.to_s.split('.').map(&:to_sym)
175
+ attr, _enclosing_type = find_filter_attribute(filter_components, media_type)
176
+ if spec[:values].is_a?(Array)
177
+ attr_coll = Attributor::Collection.of(attr.type)
178
+ attr_coll.load(spec[:values])
179
+ else
180
+ attr.load(spec[:values])
181
+ end
175
182
  else
176
- attr.load(value)
183
+ spec[:values]
177
184
  end
178
- else
179
- value
180
- end
181
- arr.push(name: attr_name, op: match[:operator], value: coerced )
185
+ accum.push(name: attr_name, op: spec[:op], value: coerced , fuzzy: spec[:fuzzies], node_object: spec[:node_object])
182
186
  end
183
- new(parsed)
187
+ new(accum)
184
188
  end
185
189
 
186
190
  def self.dump(value, **_opts)
@@ -221,7 +225,7 @@ module Praxis
221
225
  value = item[:value]
222
226
  unless value.empty?
223
227
  fuzzy_match = attr_filters[:fuzzy_match]
224
- if (value[-1] == '*' || value[0] == '*') && !fuzzy_match
228
+ if item[:fuzzy] && !item[:fuzzy].empty? && !fuzzy_match
225
229
  errors << "Fuzzy matching for #{attr_name} is not allowed (yet '*' was found in the value)"
226
230
  end
227
231
  end
@@ -0,0 +1,193 @@
1
+ require 'parslet'
2
+
3
+ module Praxis
4
+ module Extensions
5
+ module AttributeFiltering
6
+ class FilteringParams
7
+ class Condition
8
+ attr_reader :name, :op, :values
9
+ attr_accessor :parent_group
10
+
11
+ # For operands with a single or no values: Incoming data is a hash with name and op
12
+ # For Operands with multiple values: Incoming data is an array of hashes
13
+ # First hash has the spec (i.e., name and op)
14
+ # The rest of the hashes contain a value each (a hash with value: X each).
15
+ # Example: [{:name=>"multi"@0, :op=>"="@5}, {:value=>"1"@6}, {:value=>"2"@8}]
16
+ def initialize(triad:, parent_group:)
17
+ @parent_group = parent_group
18
+ if triad.is_a? Array # several values coming in
19
+ spec, *values = triad
20
+ @name = spec[:name].to_sym
21
+ @op = spec[:op].to_s
22
+
23
+ if values.empty?
24
+ @values = ""
25
+ @fuzzies = nil
26
+ elsif values.size == 1
27
+ raw_val = values.first[:value].to_s
28
+ @values, @fuzzies = _compute_fuzzy(values.first[:value].to_s)
29
+ else
30
+ @values = []
31
+ @fuzzies = []
32
+ results = values.each do|e|
33
+ val, fuz = _compute_fuzzy(e[:value].to_s)
34
+ @values.push val
35
+ @fuzzies.push fuz
36
+ end
37
+ end
38
+ else # No values for the operand
39
+ @name = triad[:name].to_sym
40
+ @op = triad[:op].to_s
41
+ if ['!','!!'].include?(@op)
42
+ @values, @fuzzies = [nil, nil]
43
+ else
44
+ # Value operand without value? => convert it to empty string
45
+ raise "Interesting, didn't know this could happen. Oops!" if triad[:value].is_a?(Array) && !triad[:value].empty?
46
+ if triad[:value] == []
47
+ @values, @fuzzies = ['', nil]
48
+ else
49
+ @values, @fuzzies = _compute_fuzzy(triad[:value].to_s)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ # Takes a raw val, and spits out the output val (unescaped), and the fuzzy definition
55
+ def _compute_fuzzy(raw_val)
56
+ starting = raw_val[0] == '*'
57
+ ending = raw_val[-1] == '*'
58
+ newval, fuzzy = if starting && ending
59
+ [raw_val[1..-2], :start_end]
60
+ elsif starting
61
+ [raw_val[1..-1], :start]
62
+ elsif ending
63
+ [raw_val[0..-2], :end]
64
+ else
65
+ [raw_val,nil]
66
+ end
67
+ newval = CGI.unescape(newval) if newval
68
+ [newval,fuzzy]
69
+ end
70
+ def flattened_conditions
71
+ [{name: @name, op: @op, values: @values, fuzzies: @fuzzies, node_object: self}]
72
+ end
73
+
74
+ # Dumps the value, marking where the fuzzy might be, and removing the * to differentiate from literals
75
+ def _dump_value(val,fuzzy)
76
+ case fuzzy
77
+ when nil
78
+ val
79
+ when :start_end
80
+ '{*}' + val + '{*}'
81
+ when :start
82
+ '{*}' + val
83
+ when :end
84
+ val +'{*}'
85
+ end
86
+ end
87
+ def dump
88
+ vals = if values.is_a? Array
89
+ dumped = values.map.with_index{|val,i| _dump_value(val, @fuzzies[i])}
90
+ "[#{dumped.join(',')}]" # Purposedly enclose in brackets to make sure we differentiate
91
+ else
92
+ (values == '') ? '""' : _dump_value(values,@fuzzies) # Dump the empty string explicitly with quotes if we've converted no value to empty string
93
+ end
94
+ "#{name}#{op}#{vals}"
95
+ end
96
+ end
97
+
98
+ # An Object that represents an AST tree for either an OR or an AND conditions
99
+ # to be applied to its items children
100
+ class ConditionGroup
101
+ attr_reader :items, :type
102
+ attr_accessor :parent_group
103
+ attr_accessor :associated_query # Metadata to be used by whomever is manipulating this
104
+
105
+ def self.load(node)
106
+ unless node[:o]
107
+ loaded = Condition.new(triad: node[:triad], parent_group: nil)
108
+ else
109
+ compactedl = compress_tree(node: node[:l], op: node[:o])
110
+ compactedr = compress_tree(node: node[:r], op: node[:o])
111
+ compacted = {op: node[:o], items: compactedl + compactedr }
112
+
113
+ loaded = ConditionGroup.new(**compacted, parent_group: nil)
114
+ end
115
+ loaded
116
+ end
117
+
118
+ def initialize(op:, items:, parent_group:)
119
+ @type = (op.to_s == '&') ? :and : :or
120
+ @items = items.map do |item|
121
+ if item[:op]
122
+ ConditionGroup.new(**item, parent_group: self)
123
+ else
124
+ Condition.new(triad: item[:triad], parent_group: self)
125
+ end
126
+ end
127
+ @parent_group = parent_group
128
+ end
129
+
130
+ def dump
131
+ "( " + @items.map(&:dump).join(" #{type.upcase} ") + " )"
132
+ end
133
+
134
+ # Returns an array with flat conditions from all child triad conditions
135
+ def flattened_conditions
136
+ @items.inject([]) do |accum, item|
137
+ accum + item.flattened_conditions
138
+ end
139
+ end
140
+
141
+ # Given a binary tree of operand conditions, transform it to a multi-leaf tree
142
+ # where a single condition node has potentially multiple subtrees for the same operation (instead of 2)
143
+ # For example (&, (&, a, b), (|, c, d)) => (&, a, b, (|, c, d))
144
+ def self.compress_tree(node:, op:)
145
+ if node[:triad]
146
+ return [node]
147
+ end
148
+
149
+ # It is an op node
150
+ if node[:o] == op
151
+ # compatible op as parent, collect my compacted children and return them up skipping my op
152
+ resultl = compress_tree(node: node[:l], op: op)
153
+ resultr = compress_tree(node: node[:r], op: op)
154
+ resultl+resultr
155
+ else
156
+ collected = compress_tree(node: node, op: node[:o])
157
+ [{op: node[:o], items: collected }]
158
+ end
159
+ end
160
+ end
161
+
162
+ class Parser < Parslet::Parser
163
+ root :expression
164
+ rule(:lparen) { str('(') }
165
+ rule(:rparen) { str(')') }
166
+ rule(:comma) { str(',') }
167
+ rule(:val_operator) { str('!=') | str('>=') | str('<=') | str('=') | str('<') | str('>')}
168
+ rule(:noval_operator) { str('!!') | str('!')}
169
+ rule(:and_kw) { str('&') }
170
+ rule(:or_kw) { str('|') }
171
+
172
+ def infix *args
173
+ Infix.new(*args)
174
+ end
175
+
176
+ rule(:name) { match('[a-zA-Z0-9_\.]').repeat(1) } # TODO: are these the only characters that we allow for names?
177
+ rule(:chars) { match('[^&|(),]').repeat(0).as(:value) }
178
+ rule(:value) { chars >> (comma >> chars ).repeat }
179
+
180
+ rule(:triad) {
181
+ (name.as(:name) >> val_operator.as(:op) >> value).as(:triad) |
182
+ (name.as(:name) >> noval_operator.as(:op)).as(:triad) |
183
+ lparen >> expression >> rparen
184
+ }
185
+
186
+ rule(:expression) {
187
+ infix_expression(triad, [and_kw, 2, :left], [or_kw, 1, :right])
188
+ }
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -9,7 +9,7 @@ module Praxis
9
9
  class << self
10
10
  def for(definition)
11
11
  Class.new(self) do
12
- @attr_to_column = case definition
12
+ @filters_map = case definition
13
13
  when Hash
14
14
  definition
15
15
  when Array
@@ -18,7 +18,7 @@ module Praxis
18
18
  raise "Cannot use FilterQueryBuilder.of without passing an array or a hash (Got: #{definition.class.name})"
19
19
  end
20
20
  class << self
21
- attr_reader :attr_to_column
21
+ attr_reader :filters_map
22
22
  end
23
23
  end
24
24
  end
@@ -33,13 +33,18 @@ module Praxis
33
33
  end
34
34
 
35
35
  # By default we'll simply use the incoming op and value, and will map
36
- # the attribute based on what's on the `attr_to_column` hash
36
+ # the attribute based on what's on the `filters_map` definition
37
37
  def generate(filters)
38
38
  raise "Not refactored yet!"
39
39
  seen_associations = Set.new
40
40
  filters.each do |(attr, spec)|
41
- column_name = attr_to_column[attr]
42
- raise "Filtering by #{attr} not allowed (no mapping found)" unless column_name
41
+ column_name = _mapped_filter(attr)
42
+ unless column_name
43
+ msg = "Filtering by #{attr} is not allowed. No implementation mapping defined for it has been found \
44
+ and there is not a model attribute with this name either.\n" \
45
+ "Please add a mapping for #{attr} in the `filters_mapping` method of the appropriate Resource class"
46
+ raise msg
47
+ end
43
48
  if column_name.is_a?(Proc)
44
49
  bindings = column_name.call(spec)
45
50
  # A hash of bindings, consisting of a key with column name and a value to the query value
@@ -64,9 +69,16 @@ module Praxis
64
69
  add_clause(attr: column_name, op: op, value: value)
65
70
  end
66
71
 
67
- def attr_to_column
68
- # Class method defined by the subclassing Class (using .for)
69
- self.class.attr_to_column
72
+ def _mapped_filter(name)
73
+ target = self.class.filters_map[name]
74
+ unless target
75
+ if @model.attribute_names.include?(name.to_s)
76
+ # Cache it in the filters mapping (to avoid later lookups), and return it.
77
+ self.class.filters_map[name] = name
78
+ target = name
79
+ end
80
+ end
81
+ return target
70
82
  end
71
83
 
72
84
  # Private to try to funnel all column names through `generate` that restricts
@@ -14,10 +14,9 @@ module Praxis
14
14
  module Pagination
15
15
  extend ActiveSupport::Concern
16
16
  # This PaginatedController concern should be added to controllers that have actions that define the
17
- # pagination and order parameters so that calling `paginate( query: <base_query>, table: <main_table_name> )`
18
- # would handle all the required logic for paginating, ordering and generating the Link and TotalCount headers.
19
- # This assumes that the query object are chainable and based on ActiveRecord at the moment (although that logic)
20
- # can be easily applied to other chainable query proxies.
17
+ # pagination and order parameters so that one can call the domain model to craft the query
18
+ # `domain_model.craft_pagination_query(base_query, pagination: _pagination)`
19
+ # This will handle all the required logic for paginating, ordering and generating the Link and TotalCount headers.
21
20
  #
22
21
  # Here's a simple example on how to use it for a fake Items controller
23
22
  # class Items < V1::Controllers::BaseController
@@ -29,7 +28,8 @@ module Praxis
29
28
  #
30
29
  # def index(filters: nil, pagination: nil, order: nil, **_args)
31
30
  # items = current_user.items.all
32
- # items = _craft_pagination_query( query: items)
31
+ # domain_model = self.media_type.domain_model
32
+ # items = domain_model.craft_pagination_query( query: items, pagination: _pagination)
33
33
  #
34
34
  # display(items)
35
35
  # end
@@ -71,33 +71,6 @@ module Praxis
71
71
  @_pagination = PaginationStruct.new(pagination[:paginator], pagination[:order])
72
72
  end
73
73
 
74
- # Main entrypoint: Handles all pagination pieces
75
- # takes:
76
- # * the query to build from and the table
77
- # * the request (for link header generation)
78
- # * requires the _pagination variable to be there (set by this module) to return the pagination struct
79
- def _craft_pagination_query(query:, type: :active_record)
80
- handler_klass = \
81
- case type
82
- when :active_record
83
- ActiveRecordPaginationHandler
84
- when :sequel
85
- SequelPaginationHandler
86
- else
87
- raise "Attempting to use pagination but Active Record or Sequel gems found"
88
- end
89
-
90
- # Gather and save the count if required
91
- if _pagination.paginator&.total_count
92
- _pagination.total_count = handler_klass.count(query.dup)
93
- end
94
-
95
- query = handler_klass.order(query, _pagination.order)
96
- # Maybe this is a class instance instead of a class method?...(of the appropriate AR/Sequel type)...
97
- # self.class.paginate(query, table, _pagination)
98
- handler_klass.paginate(query, _pagination)
99
- end
100
-
101
74
  def build_pagination_headers(pagination:, current_url:, current_query_params:)
102
75
  links = if pagination.paginator.by
103
76
  # We're assuming that the last element has a "symbol/string" field with the same name of the "by" pagination.