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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +103 -0
- data/LICENSE +674 -0
- data/README.md +240 -0
- data/lib/rails/surrender/config/locales/en.yml +3 -0
- data/lib/rails/surrender/controller_additions.rb +13 -73
- data/lib/rails/surrender/fields/asks.rb +50 -0
- data/lib/rails/surrender/fields/attribute.rb +37 -0
- data/lib/rails/surrender/fields/expand.rb +65 -0
- data/lib/rails/surrender/fields/field_tree.rb +71 -0
- data/lib/rails/surrender/fields/id_list.rb +38 -0
- data/lib/rails/surrender/fields/node.rb +86 -0
- data/lib/rails/surrender/menu/field.rb +15 -0
- data/lib/rails/surrender/menu/filter.rb +11 -0
- data/lib/rails/surrender/menu/menu.rb +55 -0
- data/lib/rails/surrender/model_additions.rb +47 -32
- data/lib/rails/surrender/params/list_param.rb +23 -0
- data/lib/rails/surrender/railtie.rb +23 -9
- data/lib/rails/surrender/render/resource.rb +11 -30
- data/lib/rails/surrender/selection/filtering.rb +144 -0
- data/lib/rails/surrender/selection/page_size.rb +33 -0
- data/lib/rails/surrender/selection/selected.rb +50 -0
- data/lib/rails/surrender/selection/selection.rb +70 -0
- data/lib/rails/surrender/selection/sorting.rb +61 -0
- data/lib/rails/surrender/version.rb +1 -1
- data/lib/rails/surrender.rb +13 -12
- metadata +65 -41
- data/lib/rails/surrender/config/initializers/active_record_associations_patch.rb +0 -17
- data/lib/rails/surrender/config/initializers/active_record_preloader_patch.rb +0 -20
- data/lib/rails/surrender/helpers/filter_builder.rb +0 -50
- data/lib/rails/surrender/helpers/pagination_builder.rb +0 -25
- data/lib/rails/surrender/helpers/query_param_parser.rb +0 -98
- data/lib/rails/surrender/helpers/sort_builder.rb +0 -56
- data/lib/rails/surrender/model_filter_scopes.rb +0 -56
- data/lib/rails/surrender/render/configuration/inclusion_mapper_logic.rb +0 -69
- data/lib/rails/surrender/render/configuration/instance_logic.rb +0 -88
- data/lib/rails/surrender/render/configuration.rb +0 -93
- data/lib/rails/surrender/render/count.rb +0 -21
- data/lib/rails/surrender/render/ids.rb +0 -21
- data/lib/rails/surrender/render/resource/collection.rb +0 -26
- data/lib/rails/surrender/render/resource/inclusion_mapper.rb +0 -48
- data/lib/rails/surrender/render/resource/instance.rb +0 -75
- data/test/surrender_test.rb +0 -9
- 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
|
data/lib/rails/surrender.rb
CHANGED
|
@@ -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/
|
|
19
|
-
require 'rails/surrender/
|
|
20
|
-
require 'rails/surrender/
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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/
|
|
95
|
-
- lib/rails/surrender/
|
|
96
|
-
- lib/rails/surrender/
|
|
97
|
-
- lib/rails/surrender/
|
|
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/
|
|
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.
|
|
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
|