rails-surrender 0.1.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 +97 -35
  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 = '0.1.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,98 +1,160 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-surrender
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 8.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shawn McBride
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2022-01-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionpack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '8.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
32
+ - !ruby/object:Gem::Dependency
33
+ name: activerecord
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '8.0'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '8.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9'
13
52
  - !ruby/object:Gem::Dependency
14
53
  name: kaminari
15
54
  requirement: !ruby/object:Gem::Requirement
16
55
  requirements:
17
- - - '='
56
+ - - "~>"
18
57
  - !ruby/object:Gem::Version
19
- version: 1.2.1
58
+ version: '1.2'
20
59
  type: :runtime
21
60
  prerelease: false
22
61
  version_requirements: !ruby/object:Gem::Requirement
23
62
  requirements:
24
- - - '='
63
+ - - "~>"
25
64
  - !ruby/object:Gem::Version
26
- version: 1.2.1
65
+ version: '1.2'
27
66
  - !ruby/object:Gem::Dependency
28
67
  name: oj
29
68
  requirement: !ruby/object:Gem::Requirement
30
69
  requirements:
31
- - - '='
70
+ - - "~>"
32
71
  - !ruby/object:Gem::Version
33
- version: 3.13.10
72
+ version: '3.13'
34
73
  type: :runtime
35
74
  prerelease: false
36
75
  version_requirements: !ruby/object:Gem::Requirement
37
76
  requirements:
38
- - - '='
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '3.13'
80
+ - !ruby/object:Gem::Dependency
81
+ name: railties
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
39
85
  - !ruby/object:Gem::Version
40
- version: 3.13.10
41
- description:
86
+ version: '8.0'
87
+ - - "<"
88
+ - !ruby/object:Gem::Version
89
+ version: '9'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
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.
42
103
  email: smmcbride@gmail.com
43
104
  executables: []
44
105
  extensions: []
45
106
  extra_rdoc_files: []
46
107
  files:
108
+ - CHANGELOG.md
109
+ - LICENSE
110
+ - README.md
47
111
  - lib/rails/surrender.rb
48
- - lib/rails/surrender/config/initializers/active_record_associations_patch.rb
49
- - lib/rails/surrender/config/initializers/active_record_preloader_patch.rb
50
112
  - lib/rails/surrender/config/locales/en.yml
51
113
  - lib/rails/surrender/controller_additions.rb
52
114
  - lib/rails/surrender/default_ability.rb
53
115
  - lib/rails/surrender/exceptions.rb
54
- - lib/rails/surrender/helpers/filter_builder.rb
55
- - lib/rails/surrender/helpers/pagination_builder.rb
56
- - lib/rails/surrender/helpers/query_param_parser.rb
57
- - 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
58
125
  - lib/rails/surrender/model_additions.rb
59
- - lib/rails/surrender/model_filter_scopes.rb
126
+ - lib/rails/surrender/params/list_param.rb
60
127
  - lib/rails/surrender/railtie.rb
61
- - lib/rails/surrender/render/configuration.rb
62
- - lib/rails/surrender/render/configuration/inclusion_mapper_logic.rb
63
- - lib/rails/surrender/render/configuration/instance_logic.rb
64
- - lib/rails/surrender/render/count.rb
65
- - lib/rails/surrender/render/ids.rb
66
128
  - lib/rails/surrender/render/resource.rb
67
- - lib/rails/surrender/render/resource/collection.rb
68
- - lib/rails/surrender/render/resource/inclusion_mapper.rb
69
- - lib/rails/surrender/render/resource/instance.rb
70
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
71
135
  - lib/rails/surrender/version.rb
72
- - test/surrender_test.rb
73
- - test/test_helper.rb
74
136
  homepage: https://github.com/smmcbride/rails-surrender
75
137
  licenses:
76
- - GPL-3.0
138
+ - GPL-3.0-only
77
139
  metadata:
140
+ changelog_uri: https://github.com/smmcbride/rails-surrender/blob/main/CHANGELOG.md
78
141
  rubygems_mfa_required: 'true'
79
- post_install_message:
142
+ source_code_uri: https://github.com/smmcbride/rails-surrender
80
143
  rdoc_options: []
81
144
  require_paths:
82
145
  - lib
83
146
  required_ruby_version: !ruby/object:Gem::Requirement
84
147
  requirements:
85
- - - "~>"
148
+ - - ">="
86
149
  - !ruby/object:Gem::Version
87
- version: '3.0'
150
+ version: '3.3'
88
151
  required_rubygems_version: !ruby/object:Gem::Requirement
89
152
  requirements:
90
153
  - - ">="
91
154
  - !ruby/object:Gem::Version
92
155
  version: '0'
93
156
  requirements: []
94
- rubygems_version: 3.2.15
95
- signing_key:
157
+ rubygems_version: 4.0.16
96
158
  specification_version: 4
97
159
  summary: JSON rendering for Rails API
98
160
  test_files: []
@@ -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