rails-surrender 7.0.0 → 8.0.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -0
  3. data/LICENSE +674 -0
  4. data/README.md +240 -0
  5. data/lib/rails/surrender/config/locales/en.yml +3 -0
  6. data/lib/rails/surrender/controller_additions.rb +13 -73
  7. data/lib/rails/surrender/fields/asks.rb +50 -0
  8. data/lib/rails/surrender/fields/attribute.rb +37 -0
  9. data/lib/rails/surrender/fields/expand.rb +65 -0
  10. data/lib/rails/surrender/fields/field_tree.rb +71 -0
  11. data/lib/rails/surrender/fields/id_list.rb +38 -0
  12. data/lib/rails/surrender/fields/node.rb +86 -0
  13. data/lib/rails/surrender/menu/field.rb +15 -0
  14. data/lib/rails/surrender/menu/filter.rb +11 -0
  15. data/lib/rails/surrender/menu/menu.rb +55 -0
  16. data/lib/rails/surrender/model_additions.rb +47 -32
  17. data/lib/rails/surrender/params/list_param.rb +23 -0
  18. data/lib/rails/surrender/railtie.rb +23 -9
  19. data/lib/rails/surrender/render/resource.rb +11 -30
  20. data/lib/rails/surrender/selection/filtering.rb +144 -0
  21. data/lib/rails/surrender/selection/page_size.rb +33 -0
  22. data/lib/rails/surrender/selection/selected.rb +50 -0
  23. data/lib/rails/surrender/selection/selection.rb +70 -0
  24. data/lib/rails/surrender/selection/sorting.rb +61 -0
  25. data/lib/rails/surrender/version.rb +1 -1
  26. data/lib/rails/surrender.rb +13 -12
  27. metadata +65 -41
  28. data/lib/rails/surrender/config/initializers/active_record_associations_patch.rb +0 -17
  29. data/lib/rails/surrender/config/initializers/active_record_preloader_patch.rb +0 -20
  30. data/lib/rails/surrender/helpers/filter_builder.rb +0 -50
  31. data/lib/rails/surrender/helpers/pagination_builder.rb +0 -25
  32. data/lib/rails/surrender/helpers/query_param_parser.rb +0 -98
  33. data/lib/rails/surrender/helpers/sort_builder.rb +0 -56
  34. data/lib/rails/surrender/model_filter_scopes.rb +0 -56
  35. data/lib/rails/surrender/render/configuration/inclusion_mapper_logic.rb +0 -69
  36. data/lib/rails/surrender/render/configuration/instance_logic.rb +0 -88
  37. data/lib/rails/surrender/render/configuration.rb +0 -93
  38. data/lib/rails/surrender/render/count.rb +0 -21
  39. data/lib/rails/surrender/render/ids.rb +0 -21
  40. data/lib/rails/surrender/render/resource/collection.rb +0 -26
  41. data/lib/rails/surrender/render/resource/inclusion_mapper.rb +0 -48
  42. data/lib/rails/surrender/render/resource/instance.rb +0 -75
  43. data/test/surrender_test.rb +0 -9
  44. data/test/test_helper.rb +0 -21
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../params/list_param'
4
+ require_relative 'page_size'
5
+ require_relative 'selected'
6
+
7
+ module Rails
8
+ module Surrender
9
+ # The records one request renders, picked from a relation by the request's filter, sort and page.
10
+ class Selection
11
+ PAGE_DEFAULT = 1
12
+
13
+ # `query_params` are the request's. A controller that skips pagination passes `paginate: false`.
14
+ def initialize(query_params, paginate: true, page_size: Surrender.page_size)
15
+ @params = query_params.to_h.symbolize_keys
16
+ @paginate = paginate
17
+ @page_size = page_size
18
+ end
19
+
20
+ def count?
21
+ @params.key?(:count)
22
+ end
23
+
24
+ # Only a relation can be filtered, sorted and paged; anything else is selected as it is.
25
+ def apply(resource)
26
+ return Selected.new(records: resource) unless resource.is_a?(ActiveRecord::Relation)
27
+
28
+ selected = sorted(filtered(resource))
29
+ return Selected.new(records: selected, sort: sort) unless paged?
30
+
31
+ # Surrender's page size limit is the only one, so Kaminari's own limits mustn't shrink the page it reports.
32
+ size = @page_size.for(positive_whole_number(:per))
33
+ paged = selected.page(positive_whole_number(:page) || PAGE_DEFAULT).per(size, max_per_page: size)
34
+ Selected.new(records: paged, unpaged: selected, sort: sort, per: size)
35
+ end
36
+
37
+ private
38
+
39
+ def filtered(relation)
40
+ Params::ListParam.parse(@params[:filter], param: :filter).reduce(relation) do |filtered, term|
41
+ raise Error, I18n.t('surrender.error.query_string.incorrect_format', param: :filter) unless term.is_a?(Hash)
42
+
43
+ term.reduce(filtered) { |narrowed, (name, value)| narrowed.surrender_filter(name, value) }
44
+ end
45
+ end
46
+
47
+ def sorted(relation)
48
+ sort ? relation.surrender_sort(sort) : relation
49
+ end
50
+
51
+ # A blank sort, as API explorers send for an empty field, means no sort rather than a sort on nothing.
52
+ def sort
53
+ @params[:sort].presence
54
+ end
55
+
56
+ # A count response counts the whole selection, so it is never paged.
57
+ def paged?
58
+ @paginate && !count?
59
+ end
60
+
61
+ def positive_whole_number(param)
62
+ value = @params[param]
63
+ return if value.nil?
64
+ return value.to_i if value.to_s.match?(/\A[1-9]\d*\z/)
65
+
66
+ raise Error, I18n.t('surrender.error.query_string.pagination.invalid', param: param, value: value)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module Surrender
5
+ class Selection
6
+ # Orders a model's records by a request's sort, such as `title`, `-pages` or `author.name`. A leading - sorts
7
+ # descending; a leading + or none sorts ascending.
8
+ module Sorting
9
+ extend ActiveSupport::Concern
10
+
11
+ SORT_PREFIX = 'sort_by_'
12
+
13
+ # Resolves a request's sort against the model.
14
+ module ClassMethods
15
+ # A sort_by_ scope the model defines wins over its columns, as filter_by_ scopes the model defines win over
16
+ # generated ones. Each scope takes the direction, 'ASC' or 'DESC'.
17
+ def surrender_sort(sort)
18
+ name = sort.strip.delete_prefix('-').delete_prefix('+')
19
+ direction = sort.strip.start_with?('-') ? :desc : :asc
20
+ scope = :"#{SORT_PREFIX}#{name.tr('.', '_')}"
21
+
22
+ return public_send(scope, direction.to_s.upcase) if respond_to?(scope)
23
+ return order(name => direction) if attribute_names.include?(name)
24
+
25
+ surrender_sort_by_association(name, direction) ||
26
+ raise(Error, I18n.t('surrender.error.query_string.sort.invalid_column', param: sort))
27
+ end
28
+
29
+ # The sorts a request may name: the model's columns, the columns of its singular associations, and the sorts
30
+ # it defines.
31
+ def surrender_sort_names
32
+ associated = reflect_on_all_associations.select { |reflection| surrender_sortable?(reflection) }
33
+ .flat_map do |reflection|
34
+ reflection.klass.attribute_names.map { |attribute| "#{reflection.name}.#{attribute}" }
35
+ end
36
+ defined = methods.grep(/\A#{SORT_PREFIX}/o).map { |scope| scope.to_s.delete_prefix(SORT_PREFIX) }.sort
37
+ (attribute_names + associated + defined).uniq
38
+ end
39
+
40
+ private
41
+
42
+ # `author.name` sorts by the name of each record's author.
43
+ def surrender_sort_by_association(name, direction)
44
+ association, attribute = name.split('.', 2)
45
+ reflection = attribute && reflect_on_association(association)
46
+ return unless reflection && surrender_sortable?(reflection)
47
+ return unless reflection.klass.attribute_names.include?(attribute)
48
+
49
+ joins(reflection.name).order(reflection.klass.arel_table[attribute].public_send(direction))
50
+ end
51
+
52
+ # Joining an association that holds many records would repeat each record once per associated one. Nothing
53
+ # can name a polymorphic association's class before records load.
54
+ def surrender_sortable?(reflection)
55
+ !reflection.collection? && !reflection.polymorphic?
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rails
4
4
  module Surrender
5
- VERSION = '7.0.0'
5
+ VERSION = '8.0.0'
6
6
  end
7
7
  end
@@ -3,6 +3,14 @@
3
3
  module Rails
4
4
  # Base namespace for the entire project
5
5
  module Surrender
6
+ class << self
7
+ # The page size the app configures with config.surrender.default_per_page and max_per_page, set at boot.
8
+ attr_writer :page_size
9
+
10
+ def page_size
11
+ @page_size ||= Selection::PageSize.new
12
+ end
13
+ end
6
14
  end
7
15
  end
8
16
 
@@ -12,20 +20,13 @@ require 'oj'
12
20
  require 'rails/surrender/default_ability'
13
21
  require 'rails/surrender/controller_additions'
14
22
  require 'rails/surrender/exceptions'
23
+ require 'rails/surrender/fields/field_tree'
24
+ require 'rails/surrender/menu/menu'
15
25
  require 'rails/surrender/model_additions'
16
- require 'rails/surrender/model_filter_scopes'
17
26
  require 'rails/surrender/railtie'
18
- require 'rails/surrender/render/ids'
19
- require 'rails/surrender/render/configuration'
20
- require 'rails/surrender/render/count'
27
+ require 'rails/surrender/selection/filtering'
28
+ require 'rails/surrender/selection/selection'
29
+ require 'rails/surrender/selection/sorting'
21
30
  require 'rails/surrender/render/resource'
22
31
  require 'rails/surrender/response'
23
32
  require 'rails/surrender/version'
24
-
25
- # Load Surrender specific error messages
26
- locale_path = Dir.glob("#{__dir__}/surrender/config/locales/*.{rb,yml}")
27
- I18n.load_path += locale_path unless I18n.load_path.include?(locale_path)
28
-
29
- # ActiveRecord Patches required for proper functionality
30
- require 'rails/surrender/config/initializers/active_record_associations_patch'
31
- require 'rails/surrender/config/initializers/active_record_preloader_patch'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-surrender
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0
4
+ version: 8.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shawn McBride
@@ -13,117 +13,141 @@ dependencies:
13
13
  name: actionpack
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ - - "<"
17
20
  - !ruby/object:Gem::Version
18
- version: 7.0.0
21
+ version: '9'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
- - - "~>"
26
+ - - ">="
24
27
  - !ruby/object:Gem::Version
25
- version: 7.0.0
28
+ version: '8.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: activerecord
28
34
  requirement: !ruby/object:Gem::Requirement
29
35
  requirements:
30
- - - "~>"
36
+ - - ">="
31
37
  - !ruby/object:Gem::Version
32
- version: 7.0.0
38
+ version: '8.0'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9'
33
42
  type: :runtime
34
43
  prerelease: false
35
44
  version_requirements: !ruby/object:Gem::Requirement
36
45
  requirements:
37
- - - "~>"
46
+ - - ">="
38
47
  - !ruby/object:Gem::Version
39
- version: 7.0.0
48
+ version: '8.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9'
40
52
  - !ruby/object:Gem::Dependency
41
53
  name: kaminari
42
54
  requirement: !ruby/object:Gem::Requirement
43
55
  requirements:
44
- - - '='
56
+ - - "~>"
45
57
  - !ruby/object:Gem::Version
46
- version: 1.2.1
58
+ version: '1.2'
47
59
  type: :runtime
48
60
  prerelease: false
49
61
  version_requirements: !ruby/object:Gem::Requirement
50
62
  requirements:
51
- - - '='
63
+ - - "~>"
52
64
  - !ruby/object:Gem::Version
53
- version: 1.2.1
65
+ version: '1.2'
54
66
  - !ruby/object:Gem::Dependency
55
67
  name: oj
56
68
  requirement: !ruby/object:Gem::Requirement
57
69
  requirements:
58
- - - '='
70
+ - - "~>"
59
71
  - !ruby/object:Gem::Version
60
- version: 3.13.10
72
+ version: '3.13'
61
73
  type: :runtime
62
74
  prerelease: false
63
75
  version_requirements: !ruby/object:Gem::Requirement
64
76
  requirements:
65
- - - '='
77
+ - - "~>"
66
78
  - !ruby/object:Gem::Version
67
- version: 3.13.10
79
+ version: '3.13'
68
80
  - !ruby/object:Gem::Dependency
69
81
  name: railties
70
82
  requirement: !ruby/object:Gem::Requirement
71
83
  requirements:
72
- - - "~>"
84
+ - - ">="
73
85
  - !ruby/object:Gem::Version
74
- version: 7.0.0
86
+ version: '8.0'
87
+ - - "<"
88
+ - !ruby/object:Gem::Version
89
+ version: '9'
75
90
  type: :runtime
76
91
  prerelease: false
77
92
  version_requirements: !ruby/object:Gem::Requirement
78
93
  requirements:
79
- - - "~>"
94
+ - - ">="
80
95
  - !ruby/object:Gem::Version
81
- version: 7.0.0
96
+ version: '8.0'
97
+ - - "<"
98
+ - !ruby/object:Gem::Version
99
+ version: '9'
100
+ description: Lets API clients shape JSON responses with query parameters for include,
101
+ exclude, filter, sort, pagination, counts and id lists, while eager loading to avoid
102
+ N+1 queries.
82
103
  email: smmcbride@gmail.com
83
104
  executables: []
84
105
  extensions: []
85
106
  extra_rdoc_files: []
86
107
  files:
108
+ - CHANGELOG.md
109
+ - LICENSE
110
+ - README.md
87
111
  - lib/rails/surrender.rb
88
- - lib/rails/surrender/config/initializers/active_record_associations_patch.rb
89
- - lib/rails/surrender/config/initializers/active_record_preloader_patch.rb
90
112
  - lib/rails/surrender/config/locales/en.yml
91
113
  - lib/rails/surrender/controller_additions.rb
92
114
  - lib/rails/surrender/default_ability.rb
93
115
  - lib/rails/surrender/exceptions.rb
94
- - lib/rails/surrender/helpers/filter_builder.rb
95
- - lib/rails/surrender/helpers/pagination_builder.rb
96
- - lib/rails/surrender/helpers/query_param_parser.rb
97
- - lib/rails/surrender/helpers/sort_builder.rb
116
+ - lib/rails/surrender/fields/asks.rb
117
+ - lib/rails/surrender/fields/attribute.rb
118
+ - lib/rails/surrender/fields/expand.rb
119
+ - lib/rails/surrender/fields/field_tree.rb
120
+ - lib/rails/surrender/fields/id_list.rb
121
+ - lib/rails/surrender/fields/node.rb
122
+ - lib/rails/surrender/menu/field.rb
123
+ - lib/rails/surrender/menu/filter.rb
124
+ - lib/rails/surrender/menu/menu.rb
98
125
  - lib/rails/surrender/model_additions.rb
99
- - lib/rails/surrender/model_filter_scopes.rb
126
+ - lib/rails/surrender/params/list_param.rb
100
127
  - lib/rails/surrender/railtie.rb
101
- - lib/rails/surrender/render/configuration.rb
102
- - lib/rails/surrender/render/configuration/inclusion_mapper_logic.rb
103
- - lib/rails/surrender/render/configuration/instance_logic.rb
104
- - lib/rails/surrender/render/count.rb
105
- - lib/rails/surrender/render/ids.rb
106
128
  - lib/rails/surrender/render/resource.rb
107
- - lib/rails/surrender/render/resource/collection.rb
108
- - lib/rails/surrender/render/resource/inclusion_mapper.rb
109
- - lib/rails/surrender/render/resource/instance.rb
110
129
  - lib/rails/surrender/response.rb
130
+ - lib/rails/surrender/selection/filtering.rb
131
+ - lib/rails/surrender/selection/page_size.rb
132
+ - lib/rails/surrender/selection/selected.rb
133
+ - lib/rails/surrender/selection/selection.rb
134
+ - lib/rails/surrender/selection/sorting.rb
111
135
  - lib/rails/surrender/version.rb
112
- - test/surrender_test.rb
113
- - test/test_helper.rb
114
136
  homepage: https://github.com/smmcbride/rails-surrender
115
137
  licenses:
116
- - GPL-3.0
138
+ - GPL-3.0-only
117
139
  metadata:
140
+ changelog_uri: https://github.com/smmcbride/rails-surrender/blob/main/CHANGELOG.md
118
141
  rubygems_mfa_required: 'true'
142
+ source_code_uri: https://github.com/smmcbride/rails-surrender
119
143
  rdoc_options: []
120
144
  require_paths:
121
145
  - lib
122
146
  required_ruby_version: !ruby/object:Gem::Requirement
123
147
  requirements:
124
- - - "~>"
148
+ - - ">="
125
149
  - !ruby/object:Gem::Version
126
- version: '3.0'
150
+ version: '3.3'
127
151
  required_rubygems_version: !ruby/object:Gem::Requirement
128
152
  requirements:
129
153
  - - ">="
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
- #
3
- # ActiveRecord::Associations.class_eval do
4
- # # patching to prevent AssociationNotFoundError.
5
- # # if there is no association just return nil instead
6
- # def association(name) # :nodoc:
7
- # super
8
- # rescue AssociationNotFoundError
9
- # return nil
10
- # end
11
- # end
12
- #
13
- # ActiveRecord::Relation.class_eval do
14
- # def eager_loading?
15
- # false
16
- # end
17
- # end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
- #
3
- # ActiveRecord::Associations::Preloader.class_eval do
4
- # # patching to ignore nil response from association method (also patched)
5
- #
6
- # # if there is no association just return nil instead
7
- # def grouped_records(association, records)
8
- # h = {}
9
- # records.each do |record|
10
- # next unless record
11
- #
12
- # assoc = record.association(association)
13
- # next if assoc.nil?
14
- #
15
- # klasses = h[assoc.reflection] ||= {}
16
- # (klasses[assoc.klass] ||= []) << record
17
- # end
18
- # h
19
- # end
20
- # end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rails
4
- module Surrender
5
- # apply filtering directives to the given resource, based on the given filter controls
6
- class FilterBuilder
7
- attr_reader :resource, :filter
8
-
9
- def initialize(resource:, filter:)
10
- @resource = resource
11
- @filter = filter
12
- end
13
-
14
- def build!
15
- return resource unless resource.is_a?(ActiveRecord::Relation)
16
-
17
- filter.each do |term|
18
- scope, value = term.first
19
-
20
- send_filter_for(scope, value)
21
- end
22
-
23
- resource
24
- end
25
-
26
- private
27
-
28
- def send_filter_for(scope, value)
29
- if resource.respond_to?(filter_method(scope))
30
- # filter exists on model?
31
- @resource = @resource.send(filter_method(scope), value)
32
- elsif resource.respond_to?(filter_method_id(scope))
33
- # resolved it by appending _id?
34
- @resource = @resource.send(filter_method_id(scope), value)
35
- else
36
- raise Error, I18n.t('surrender.error.query_string.filter.not_available', param: scope)
37
- end
38
- end
39
-
40
- # prepend filter_by so that only filter_by scope methods are reachable.
41
- def filter_method(scope)
42
- "filter_by_#{scope}".gsub('.', '_').to_sym
43
- end
44
-
45
- def filter_method_id(scope)
46
- "#{filter_method(scope)}_id".to_sym
47
- end
48
- end
49
- end
50
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rails
4
- module Surrender
5
- # apply pagination directives to the given resource, based on the given pagination controls
6
- class PaginationBuilder
7
- attr_reader :resource, :pagination
8
-
9
- def initialize(resource:, pagination:)
10
- @resource = resource
11
- @pagination = pagination
12
- end
13
-
14
- def build!
15
- return resource unless paginatable?
16
-
17
- resource.page(pagination.page).per(pagination.per)
18
- end
19
-
20
- def paginatable?
21
- resource.respond_to?(:page)
22
- end
23
- end
24
- end
25
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rails
4
- module Surrender
5
- # parse the requests query_params for surrender's controls and validate the formatting
6
- class QueryParamParser
7
- attr_reader :query_params
8
-
9
- COUNT_PARAM = :count
10
- EXCLUDE_PARAM = :exclude
11
- FILTER_PARAM = :filter
12
- IDS_PARAM = :ids
13
- INCLUDE_PARAM = :include
14
- SORT_PARAM = :sort
15
- PAGE_PARAM = :page
16
- PER_PARAM = :per
17
-
18
- PER_PAGE_DEFAULT = 50
19
- PAGE_DEFAULT = 1
20
-
21
- Sort = Struct.new(:request, :direction, :association, :attribute, :scope_method, keyword_init: true)
22
- Pagination = Struct.new(:page, :per, keyword_init: true)
23
-
24
- def initialize(query_params)
25
- @query_params = query_params
26
- end
27
-
28
- def include
29
- @include ||= parse_yml(query_params[INCLUDE_PARAM], :include)
30
- end
31
-
32
- def exclude
33
- @exclude ||= parse_yml(query_params[EXCLUDE_PARAM], :exclude)
34
- end
35
-
36
- def sort?
37
- query_params.key? SORT_PARAM
38
- end
39
-
40
- def sort
41
- @sort ||= begin
42
- sort = String.new(query_params[SORT_PARAM] || '')
43
-
44
- direction_flag = ['+', '-'].include?(sort[0, 1]) ? sort.slice!(0) : '+'
45
- direction = direction_flag == '-' ? 'DESC' : 'ASC'
46
-
47
- scope_method = "sort_by_#{sort}".gsub('.', '_').to_sym
48
- association, attribute = sort_attributes(sort)
49
-
50
- Sort.new(request: query_params[SORT_PARAM], direction: direction, attribute: attribute,
51
- association: association, scope_method: scope_method)
52
- end
53
- end
54
-
55
- def filter?
56
- filter.present?
57
- end
58
-
59
- def filter
60
- @filter ||= parse_yml(query_params[FILTER_PARAM], :filter)
61
- end
62
-
63
- def ids?
64
- query_params.key?(IDS_PARAM)
65
- end
66
-
67
- def count?
68
- query_params.key?(COUNT_PARAM)
69
- end
70
-
71
- def paginate?
72
- query_params.key? PAGE_PARAM
73
- end
74
-
75
- def pagination
76
- @pagination ||= Pagination.new(
77
- page: query_params[PAGE_PARAM]&.to_i || PAGE_DEFAULT,
78
- per: query_params[PER_PARAM]&.to_i || PER_PAGE_DEFAULT
79
- )
80
- end
81
-
82
- private
83
-
84
- def sort_attributes(sort)
85
- return sort.split('.') if sort.include? '.'
86
-
87
- ['', sort]
88
- end
89
-
90
- def parse_yml(query_string, action)
91
- query_string ||= '' # empty string in case nil is passed
92
- Psych.safe_load("[#{query_string.gsub(/(,|:)/, '\1 ')}]")
93
- rescue StandardError
94
- raise Error, I18n.t('surrender.error.query_string.incorrect_format', param: action)
95
- end
96
- end
97
- end
98
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rails
4
- module Surrender
5
- # apply sort directives to the given resource, based on the given sort controls
6
- class SortBuilder
7
- attr_reader :resource, :sort
8
-
9
- def initialize(resource:, sort:)
10
- @resource = resource
11
- @sort = sort
12
- end
13
-
14
- def build!
15
- return resource unless resource.is_a? ActiveRecord::Relation
16
-
17
- return resource_attribute_order if resource_has_attribute?
18
-
19
- return scope_method_order if resource_has_scope_method?
20
-
21
- return association_attribute_order if resource_has_association_with_attribute?
22
-
23
- raise Error, I18n.t('surrender.error.query_string.sort.invalid_column', param: sort.request)
24
- end
25
-
26
- private
27
-
28
- def resource_has_attribute?
29
- resource.klass.attribute_names.include?(sort.attribute)
30
- end
31
-
32
- def resource_attribute_order
33
- resource.order(sort.attribute => sort.direction)
34
- end
35
-
36
- def resource_has_scope_method?
37
- resource.respond_to?(sort.scope_method)
38
- end
39
-
40
- def scope_method_order
41
- resource.send(sort.scope_method, sort.direction)
42
- end
43
-
44
- def resource_has_association_with_attribute?
45
- resource.reflections.keys.include?(sort.association) &&
46
- resource.reflect_on_association(sort.association).klass.attribute_names.include?(sort.attribute)
47
- end
48
-
49
- def association_attribute_order
50
- table_name = resource.reflect_on_association(sort.association).klass.table_name
51
- resource.joins(sort.association.to_sym)
52
- .order("#{table_name}.#{sort.attribute} #{sort.direction}")
53
- end
54
- end
55
- end
56
- end