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

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