typical_sort 0.0.2.pre.rc

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b87dbf5641b61d4d91226f1e1ac41d0023111feff95e66ecafe556811d1ba35a
4
+ data.tar.gz: c8938919d3aacb024d5b053e2e447d9c1431dd41f1be11b9face066a1c7c9676
5
+ SHA512:
6
+ metadata.gz: 59552e79bd79dd6467be238ecabdcccf64bccba01877718e1a6d00178011ba93926eb57af60bb61d8834f77161e154e1db01f510e18954319b6c654050dbca97
7
+ data.tar.gz: 3ef98d235347bb12e36532bc9314b97981c8ed9e99ca477563e617f394065ac20db1b32baab503c2c7a347315a2361710e1a872eaf371a650041c34c67564f5e
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Apsis Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # Typical Sort
2
+
3
+ Predictable, allowlisted sorting for Rails controllers.
4
+
5
+ `typical_sort` is intentionally small. It does one thing: turns request sort params into safe ActiveRecord ordering.
6
+
7
+ ## Installation
8
+
9
+ ```rb
10
+ gem "typical_sort"
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```rb
16
+ class PatientsController < ApplicationController
17
+ include TypicalSort
18
+
19
+ typical_sort do
20
+ default :created_at, :desc
21
+
22
+ sort :created_at
23
+ sort :last_name
24
+ sort "account.name"
25
+ sort "insurance_plans.name", aggregate: :directional
26
+ end
27
+
28
+ def index
29
+ @patients = sort_records(Patient.all)
30
+ end
31
+ end
32
+ ```
33
+
34
+ Supported params:
35
+
36
+ ```txt
37
+ ?sort=created_at&sort_dir=desc
38
+ ?sort=-created_at
39
+ ?sort=account.name
40
+ ?sort=insurance_plans.name&sort_dir=asc
41
+ ```
42
+
43
+ Only declared sorts are allowed. Unknown request sort params are ignored by default, or raised as `TypicalSort::InvalidSort` when configured with `invalid_sort = :raise`.
44
+
45
+ ## Base and association sorting
46
+
47
+ Declare base-table columns with symbols:
48
+
49
+ ```rb
50
+ sort :last_name
51
+ sort :created_at
52
+ ```
53
+
54
+ Declare one-level association paths with strings:
55
+
56
+ ```rb
57
+ sort "account.name"
58
+ ```
59
+
60
+ Association support:
61
+
62
+ - `belongs_to`
63
+ - `has_one`
64
+ - `has_many`, with an aggregate
65
+ - `has_many :through`, with an aggregate
66
+ - `has_and_belongs_to_many`, with an aggregate
67
+ - polymorphic `has_many`, with an aggregate
68
+ - STI associations using normal Rails reflections
69
+
70
+ Polymorphic `belongs_to` paths are not supported because there is no single target table to order by. Nested association paths such as `"account.organization.name"` are also not supported.
71
+
72
+ ## Aggregate sorting
73
+
74
+ Collection associations need one sortable value per parent record. Declare an aggregate explicitly:
75
+
76
+ ```rb
77
+ sort "insurance_plans.name", aggregate: :directional
78
+ sort "payments.amount_cents", aggregate: :sum
79
+ sort "comments.id", aggregate: :count
80
+ ```
81
+
82
+ Supported aggregate values:
83
+
84
+ - `:directional` — `MIN` for ascending, `MAX` for descending
85
+ - `:min`
86
+ - `:max`
87
+ - `:sum`
88
+ - `:avg`
89
+ - `:count`
90
+
91
+ ## Scope sorting
92
+
93
+ Use `scope: true` when a sort needs custom ordering logic:
94
+
95
+ ```rb
96
+ class Book < ApplicationRecord
97
+ scope :by_publication, ->(dir) {
98
+ order(year_published: dir).order(author_name: :asc)
99
+ }
100
+ end
101
+
102
+ class BooksController < ApplicationController
103
+ include TypicalSort
104
+
105
+ typical_sort do
106
+ sort :by_publication, scope: true
107
+ end
108
+ end
109
+ ```
110
+
111
+ Scope sorts always receive the resolved direction as the first argument: `:asc` or `:desc`.
112
+
113
+ ## Pagination
114
+
115
+ Apply `sort_records` before paginating so the database sorts the full filtered relation, then the pagination library applies `LIMIT` / `OFFSET` to the sorted relation.
116
+
117
+ ### Pagy
118
+
119
+ ```rb
120
+ class PatientsController < ApplicationController
121
+ include Pagy::Backend
122
+ include TypicalSort
123
+
124
+ typical_sort do
125
+ default :created_at, :desc
126
+ sort :created_at
127
+ sort :last_name
128
+ sort "insurance_plans.name", aggregate: :directional
129
+ end
130
+
131
+ def index
132
+ patients = Patient.where(active: true)
133
+ patients = sort_records(patients)
134
+
135
+ @pagy, @patients = pagy(patients)
136
+ end
137
+ end
138
+ ```
139
+
140
+ ### Kaminari
141
+
142
+ ```rb
143
+ class PatientsController < ApplicationController
144
+ include TypicalSort
145
+
146
+ typical_sort do
147
+ default :created_at, :desc
148
+ sort :created_at
149
+ sort :last_name
150
+ sort "insurance_plans.name", aggregate: :directional
151
+ end
152
+
153
+ def index
154
+ @patients = Patient
155
+ .where(active: true)
156
+ .then { |records| sort_records(records) }
157
+ .page(params[:page])
158
+ .per(25)
159
+ end
160
+ end
161
+ ```
162
+
163
+ Avoid paginating first and sorting second:
164
+
165
+ ```rb
166
+ # Avoid: sorts only after the relation is already constrained by pagination.
167
+ sort_records(Patient.page(params[:page]))
168
+ ```
169
+
170
+ ## Configuration
171
+
172
+ ```rb
173
+ TypicalSort.configure do |config|
174
+ config.sort_param = :sort
175
+ config.direction_param = :sort_dir
176
+ config.default_direction = :asc
177
+ config.invalid_sort = :ignore # or :raise
178
+ config.tie_breaker = :primary_key
179
+ config.nulls = {
180
+ asc: :last,
181
+ desc: :first
182
+ }
183
+ end
184
+ ```
185
+
186
+ `nulls` values must be `:first` or `:last`.
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ bundle install
192
+ bundle exec appraisal install
193
+ bundle exec appraisal rspec
194
+ bundle exec standardrb
195
+ ```
196
+
197
+ ### Releasing
198
+
199
+ Create a release from `main`:
200
+
201
+ ```sh
202
+ bin/release {major|minor|patch|pre}
203
+ git push --follow-tags
204
+ ```
205
+
206
+ The release script validates the repository, bumps the version, creates a git tag.
207
+
208
+ Publishing to RubyGems and creating a GitHub Release are handled automatically by GitHub Actions.
209
+
210
+ ## Contributing
211
+
212
+ Bug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/typical_sort.
213
+
214
+ ## License
215
+
216
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
217
+
218
+
219
+ ---
220
+
221
+ # Built by Apsis
222
+
223
+ [![apsis](https://s3-us-west-2.amazonaws.com/apsiscdn/apsis.png)](https://www.apsis.io)
224
+
225
+ `typical_sort` was built by Apsis Labs. We love sharing what we build! Check out our [other libraries on Github](https://github.com/apsislabs), and if you like our work you can [hire us](https://www.apsis.io) to build your vision.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "standard/rake"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task default: %i[standard spec]
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypicalSort
4
+ class AggregateSorter
5
+ VALID_AGGREGATES = %i[min max sum avg count directional].freeze
6
+
7
+ attr_reader :records, :relation_name, :reflection, :column_name, :direction, :definition, :configuration
8
+
9
+ def initialize(records:, relation_name:, reflection:, column_name:, direction:, definition:, configuration:)
10
+ @records = records
11
+ @relation_name = relation_name.to_sym
12
+ @reflection = reflection
13
+ @column_name = column_name.to_s
14
+ @direction = direction.to_sym
15
+ @definition = definition
16
+ @configuration = configuration
17
+ end
18
+
19
+ def call
20
+ aggregate = aggregate_sql_function
21
+ connection = records.klass.connection
22
+ base_table_name = records.klass.table_name
23
+ base_pk = records.klass.primary_key
24
+ quoted_base_table = connection.quote_table_name(base_table_name)
25
+ quoted_base_pk = connection.quote_column_name(base_pk)
26
+ sort_value_sql = value_sql(connection)
27
+
28
+ base_ids = records
29
+ .except(:select, :order)
30
+ .select("#{quoted_base_table}.#{quoted_base_pk}")
31
+
32
+ filtered_ids = records
33
+ .except(:select, :order, :limit, :offset, :group)
34
+ .select("#{quoted_base_table}.#{quoted_base_pk}")
35
+
36
+ sort_subquery = records.klass
37
+ .where(base_pk => filtered_ids)
38
+ .left_outer_joins(relation_name)
39
+ .select(
40
+ "#{quoted_base_table}.#{quoted_base_pk} AS typical_sort_record_id",
41
+ "#{aggregate}(#{sort_value_sql}) AS typical_sort_value"
42
+ )
43
+ .group("#{quoted_base_table}.#{quoted_base_pk}")
44
+
45
+ records.klass
46
+ .where(base_pk => base_ids)
47
+ .joins(<<~SQL.squish)
48
+ LEFT JOIN (#{sort_subquery.to_sql}) typical_sort_scope
49
+ ON typical_sort_scope.typical_sort_record_id = #{quoted_base_table}.#{quoted_base_pk}
50
+ SQL
51
+ .order(Arel.sql("typical_sort_scope.typical_sort_value #{sql_direction} NULLS #{nulls_position}"))
52
+ end
53
+
54
+ private
55
+
56
+ def aggregate_sql_function
57
+ aggregate = definition.aggregate || :directional
58
+ raise ArgumentError, "Unsupported aggregate: #{aggregate}" unless VALID_AGGREGATES.include?(aggregate)
59
+
60
+ case aggregate
61
+ when :directional
62
+ (direction == :desc) ? "MAX" : "MIN"
63
+ else
64
+ aggregate.to_s.upcase
65
+ end
66
+ end
67
+
68
+ def value_sql(connection)
69
+ return "*" if definition.aggregate == :count
70
+
71
+ related_table = connection.quote_table_name(reflection.klass.table_name)
72
+ related_column = connection.quote_column_name(column_name)
73
+ "#{related_table}.#{related_column}"
74
+ end
75
+
76
+ def sql_direction
77
+ (direction == :desc) ? "DESC" : "ASC"
78
+ end
79
+
80
+ def nulls_position
81
+ nulls = definition.nulls || configuration.nulls.fetch(direction)
82
+ raise ArgumentError, "Unsupported nulls position: #{nulls.inspect}" unless %i[first last].include?(nulls)
83
+
84
+ nulls.to_s.upcase
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypicalSort
4
+ class Configuration
5
+ attr_accessor :sort_param,
6
+ :direction_param,
7
+ :default_direction,
8
+ :invalid_sort,
9
+ :tie_breaker,
10
+ :nulls
11
+
12
+ def initialize
13
+ @sort_param = :sort
14
+ @direction_param = :sort_dir
15
+ @default_direction = :asc
16
+ @invalid_sort = :ignore
17
+ @tie_breaker = :primary_key
18
+ @nulls = {
19
+ asc: :last,
20
+ desc: :first
21
+ }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module TypicalSort
6
+ class Engine < ::Rails::Engine
7
+ end
8
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypicalSort
4
+ class Params
5
+ attr_reader :params, :configuration, :default_attribute, :default_direction
6
+
7
+ def initialize(params:, configuration:, default_attribute:, default_direction:)
8
+ @params = params
9
+ @configuration = configuration
10
+ @default_attribute = default_attribute
11
+ @default_direction = default_direction
12
+ end
13
+
14
+ def attribute
15
+ explicit_attribute || default_attribute
16
+ end
17
+
18
+ def direction
19
+ explicit_direction || normalized_direction(default_direction) || configuration.default_direction
20
+ end
21
+
22
+ def explicit?
23
+ raw_sort.present?
24
+ end
25
+
26
+ private
27
+
28
+ def raw_sort
29
+ params[configuration.sort_param].presence
30
+ end
31
+
32
+ def raw_direction
33
+ params[configuration.direction_param].presence
34
+ end
35
+
36
+ def explicit_attribute
37
+ return nil if raw_sort.blank?
38
+
39
+ raw_sort.to_s.delete_prefix("-")
40
+ end
41
+
42
+ def explicit_direction
43
+ return :desc if raw_sort.to_s.start_with?("-")
44
+
45
+ normalized_direction(raw_direction)
46
+ end
47
+
48
+ def normalized_direction(value)
49
+ return nil if value.blank?
50
+
51
+ (value.to_s.downcase == "desc") ? :desc : :asc
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypicalSort
4
+ class PathResolver
5
+ attr_reader :records, :path
6
+
7
+ def initialize(records:, path:)
8
+ @records = records
9
+ @path = path.to_s
10
+ end
11
+
12
+ def association_name
13
+ return nil unless association_sort?
14
+
15
+ path.split(".", 2).first
16
+ end
17
+
18
+ def column_name
19
+ association_sort? ? path.split(".", 2).last : path
20
+ end
21
+
22
+ def nested_association_sort?
23
+ column_name.include?(".")
24
+ end
25
+
26
+ def reflection
27
+ return nil unless association_name
28
+
29
+ records.klass.reflect_on_association(association_name.to_sym)
30
+ end
31
+
32
+ def association_sort?
33
+ path.include?(".")
34
+ end
35
+
36
+ def collection_association?
37
+ %i[has_many has_and_belongs_to_many].include?(reflection&.macro)
38
+ end
39
+
40
+ def relation_association?
41
+ reflection.present? && !collection_association?
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypicalSort
4
+ SortDefinition = Struct.new(:name, :aggregate, :nulls, :scope, keyword_init: true) do
5
+ def key
6
+ name.to_s
7
+ end
8
+
9
+ def aggregate?
10
+ !aggregate.nil?
11
+ end
12
+
13
+ def scope?
14
+ scope == true
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "typical_sort/sort_definition"
4
+
5
+ module TypicalSort
6
+ class SortSet
7
+ attr_reader :definitions, :default_attribute, :default_direction
8
+
9
+ def initialize
10
+ @definitions = {}
11
+ end
12
+
13
+ def default(attribute, direction = nil)
14
+ @default_attribute = attribute
15
+ @default_direction = direction&.to_sym
16
+ sort(attribute) unless definitions.key?(attribute.to_s)
17
+ end
18
+
19
+ def sort(attribute, aggregate: nil, nulls: nil, scope: false)
20
+ definition = SortDefinition.new(name: attribute.to_s, aggregate: aggregate&.to_sym, nulls: nulls&.to_sym, scope: scope)
21
+ definitions[definition.key] = definition
22
+ end
23
+
24
+ def fetch(attribute)
25
+ definitions[attribute.to_s]
26
+ end
27
+
28
+ def keys
29
+ definitions.keys
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "typical_sort/aggregate_sorter"
4
+ require "typical_sort/path_resolver"
5
+
6
+ module TypicalSort
7
+ class Sorter
8
+ attr_reader :records, :definition, :direction, :configuration
9
+
10
+ def initialize(records:, definition:, direction:, configuration:)
11
+ @records = records
12
+ @definition = definition
13
+ @direction = direction.to_sym
14
+ @configuration = configuration
15
+ end
16
+
17
+ def call
18
+ return records.public_send(definition.name, direction) if definition.scope?
19
+
20
+ resolver = PathResolver.new(records: records, path: definition.key)
21
+ validate_resolver!(resolver)
22
+
23
+ if resolver.collection_association? || definition.aggregate?
24
+ AggregateSorter.new(
25
+ records: records,
26
+ relation_name: resolver.association_name,
27
+ reflection: resolver.reflection,
28
+ column_name: resolver.column_name,
29
+ direction: direction,
30
+ definition: definition,
31
+ configuration: configuration
32
+ ).call
33
+ elsif resolver.relation_association?
34
+ sort_relation_association(resolver)
35
+ else
36
+ sort_base_column(resolver.column_name)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def sort_base_column(column_name)
43
+ table = records.klass.arel_table
44
+ node = table[column_name]
45
+ records.order(order_node(node))
46
+ end
47
+
48
+ def sort_relation_association(resolver)
49
+ table = resolver.reflection.klass.arel_table
50
+ node = table[resolver.column_name]
51
+ records.left_outer_joins(resolver.association_name.to_sym).order(order_node(node))
52
+ end
53
+
54
+ def validate_resolver!(resolver)
55
+ if resolver.association_sort? && resolver.reflection.nil?
56
+ raise ArgumentError, "Unknown sort association: #{resolver.association_name}"
57
+ end
58
+
59
+ if resolver.nested_association_sort?
60
+ raise ArgumentError, "Nested sort paths are not supported: #{definition.key}"
61
+ end
62
+
63
+ if resolver.reflection&.polymorphic?
64
+ raise ArgumentError, "Polymorphic belongs_to sort paths are not supported: #{definition.key}"
65
+ end
66
+
67
+ if definition.aggregate? && !resolver.association_sort?
68
+ raise ArgumentError, "Aggregate sorts require an association path: #{definition.key}"
69
+ end
70
+ end
71
+
72
+ def order_node(node)
73
+ ordered = (direction == :desc) ? node.desc : node.asc
74
+ nulls = nulls_position
75
+
76
+ case nulls
77
+ when :first
78
+ ordered.nulls_first
79
+ when :last
80
+ ordered.nulls_last
81
+ else
82
+ raise ArgumentError, "Unsupported nulls position: #{nulls.inspect}"
83
+ end
84
+ end
85
+
86
+ def nulls_position
87
+ definition.nulls || configuration.nulls.fetch(direction)
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypicalSort
4
+ VERSION = "0.0.2-rc"
5
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "typical_sort/version"
5
+ require "typical_sort/engine"
6
+ require "typical_sort/configuration"
7
+ require "typical_sort/params"
8
+ require "typical_sort/sort_set"
9
+ require "typical_sort/sorter"
10
+
11
+ module TypicalSort
12
+ class Error < StandardError; end
13
+ class InvalidSort < Error; end
14
+
15
+ class << self
16
+ attr_writer :configuration
17
+
18
+ def configuration
19
+ @configuration ||= Configuration.new
20
+ end
21
+
22
+ def configure
23
+ yield configuration
24
+ end
25
+ end
26
+
27
+ extend ActiveSupport::Concern
28
+
29
+ included do
30
+ class_attribute :typical_sort_set, instance_writer: false, default: SortSet.new
31
+ end
32
+
33
+ class_methods do
34
+ def typical_sort(param: nil, direction_param: nil, &block)
35
+ set = SortSet.new
36
+ set.instance_eval(&block) if block
37
+ self.typical_sort_set = set
38
+
39
+ define_method :sort_param_name do
40
+ param || TypicalSort.configuration.sort_param
41
+ end
42
+
43
+ define_method :sort_direction_param_name do
44
+ direction_param || TypicalSort.configuration.direction_param
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def sort_records(records)
52
+ sort_params = TypicalSort::Params.new(
53
+ params: params,
54
+ configuration: request_configuration,
55
+ default_attribute: default_sorting_attribute,
56
+ default_direction: default_sorting_direction
57
+ )
58
+
59
+ attribute = sort_params.attribute
60
+ return records if attribute.blank?
61
+
62
+ definition = sort_definition_for(attribute)
63
+ return handle_invalid_sort(records, attribute) unless definition
64
+
65
+ sorted = TypicalSort::Sorter.new(
66
+ records: records,
67
+ definition: definition,
68
+ direction: sort_params.direction,
69
+ configuration: request_configuration
70
+ ).call
71
+
72
+ append_sort_tiebreaker(sorted)
73
+ end
74
+
75
+ def sort_definition_for(attribute)
76
+ typical_sort_set.fetch(attribute)
77
+ end
78
+
79
+ def handle_invalid_sort(records, attribute)
80
+ case TypicalSort.configuration.invalid_sort
81
+ when :raise
82
+ raise TypicalSort::InvalidSort, "Invalid sort: #{attribute}"
83
+ else
84
+ append_sort_tiebreaker(records)
85
+ end
86
+ end
87
+
88
+ def append_sort_tiebreaker(records)
89
+ return records unless TypicalSort.configuration.tie_breaker == :primary_key
90
+
91
+ table = records.klass.arel_table
92
+ records.order(table[records.klass.primary_key].asc)
93
+ end
94
+
95
+ def default_sorting_attribute
96
+ typical_sort_set.default_attribute || :created_at
97
+ end
98
+
99
+ def default_sorting_direction
100
+ typical_sort_set.default_direction || TypicalSort.configuration.default_direction
101
+ end
102
+
103
+ def request_configuration
104
+ configuration = TypicalSort.configuration.dup
105
+ configuration.sort_param = sort_param_name if respond_to?(:sort_param_name, true)
106
+ configuration.direction_param = sort_direction_param_name if respond_to?(:sort_direction_param_name, true)
107
+ configuration
108
+ end
109
+ end
metadata ADDED
@@ -0,0 +1,221 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: typical_sort
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2.pre.rc
5
+ platform: ruby
6
+ authors:
7
+ - Wyatt Kirby
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 8.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 8.0.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: appraisal
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: bump
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: bundler
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 2.2.0
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 2.2.0
68
+ - !ruby/object:Gem::Dependency
69
+ name: combustion
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: factory_bot_rails
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rails-controller-testing
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rake
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rspec-rails
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '6.0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '6.0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: search_cop
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: sqlite3
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '1.4'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '1.4'
166
+ - !ruby/object:Gem::Dependency
167
+ name: standard
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ description: A small Rails controller mixin for applying safe, composable ActiveRecord
181
+ sorting from request params.
182
+ email:
183
+ - wyatt@apsis.io
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - MIT-LICENSE
189
+ - README.md
190
+ - Rakefile
191
+ - lib/typical_sort.rb
192
+ - lib/typical_sort/aggregate_sorter.rb
193
+ - lib/typical_sort/configuration.rb
194
+ - lib/typical_sort/engine.rb
195
+ - lib/typical_sort/params.rb
196
+ - lib/typical_sort/path_resolver.rb
197
+ - lib/typical_sort/sort_definition.rb
198
+ - lib/typical_sort/sort_set.rb
199
+ - lib/typical_sort/sorter.rb
200
+ - lib/typical_sort/version.rb
201
+ homepage: https://github.com/apsislabs/typical_sort
202
+ licenses: []
203
+ metadata: {}
204
+ rdoc_options: []
205
+ require_paths:
206
+ - lib
207
+ required_ruby_version: !ruby/object:Gem::Requirement
208
+ requirements:
209
+ - - ">="
210
+ - !ruby/object:Gem::Version
211
+ version: 3.3.0
212
+ required_rubygems_version: !ruby/object:Gem::Requirement
213
+ requirements:
214
+ - - ">="
215
+ - !ruby/object:Gem::Version
216
+ version: '0'
217
+ requirements: []
218
+ rubygems_version: 3.6.9
219
+ specification_version: 4
220
+ summary: Predictable, allowlisted sorting for Rails controllers.
221
+ test_files: []