sort_param 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cbe25d60eee31370137b39d3edaa1b0e90270ce37e710fda5bd8d4d46bf09763
4
+ data.tar.gz: 174b0d2fc448a461b6eef190f80d73c20ad21f90e821647714716f35e22b05d0
5
+ SHA512:
6
+ metadata.gz: 283a35dd58cef523745ef97a097d0ba46548438b580947bc37d8754bdf326b99002f3ba145a270c7fa86978f8b0eca20ffc316b709f0c44b1cffc764dc01d888
7
+ data.tar.gz: 51b36c10d39d676274133664678c241c301050f64de7495764e1773124f5b17ecbbf8f334c1b62f6e8a943aed57e9cde8409926917e9ca7bbc0cf27bf3eb3364
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## [0.1.0]
2
+
3
+ - Initial release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Jayson Uy
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,135 @@
1
+ # SortParam
2
+
3
+ Sort records using a query parameter based on JSON API's sorting format.
4
+
5
+ ## Features
6
+
7
+ * Supports `ORDER BY` expression generation for MySQL and PG.
8
+ * Parse the sort string/expression into hash for any further processing.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'sort_param'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ ```sh
21
+ bundle install
22
+ ```
23
+
24
+ Or install it yourself as:
25
+
26
+ ```sh
27
+ gem install sort_param
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Basic
33
+
34
+
35
+ #### 1. Whitelist/define the sort fields
36
+
37
+ ```ruby
38
+ sort_param = SortParam.define do
39
+ field :first_name, nulls: :first
40
+ field :last_name
41
+ end
42
+ ```
43
+
44
+
45
+ OR we can do:
46
+
47
+ ```ruby
48
+ sort_param = SortParam::Definition.new
49
+ .field(:first_name, nulls: :first)
50
+ .field(:last_name)
51
+ ```
52
+
53
+ `field` method accepts the column name as the first argument. Any default column configuration such as `:nulls`(for `NULLS FIRST` or `NULLS LAST` sort order) follows the name.
54
+
55
+ #### 2. Parse sort fields from a parameter
56
+
57
+ The `load!` method translates a given sort string/fields parameter to an SQL ORDER BY expression or to a Hash:
58
+
59
+ ##### I. PostgreSQL example
60
+
61
+ ```ruby
62
+ sort_param.load!("+first_name,-last_name", mode: :pg)
63
+
64
+ => "first_name asc nulls first, last_name desc"
65
+ ```
66
+
67
+ ##### II. MySQL example
68
+
69
+ ```ruby
70
+ sort_param.load!("+first_name,-last_name", mode: :mysql)
71
+
72
+ => "first_name is not null, first_name asc, last_name desc"
73
+ ```
74
+
75
+ ##### III. Hash example
76
+
77
+ ```ruby
78
+ sort_param.load!("+first_name,-last_name")
79
+
80
+ => {"first_name"=>{:nulls=>:first, :direction=>:asc}, "last_name"=>{:direction=>:desc}}
81
+ ```
82
+
83
+ #### IV. Example with explicit nulls sort order
84
+
85
+ ###### Example in PG mode:
86
+
87
+ ```ruby
88
+ sort_param.load!("+first_name:nulls_last,-last_name:nulls_first", mode: :pg)
89
+
90
+ => "first_name asc nulls last, last_name desc nulls first"
91
+ ```
92
+ <br/>
93
+
94
+ ### Rails example
95
+
96
+ ```ruby
97
+ def index
98
+ users = User.all.order(order_by)
99
+ end
100
+
101
+ private
102
+
103
+ def order_by
104
+ SortParam.define do
105
+ field :first_name
106
+ field :last_name, nulls: :first
107
+ end.load!(sort_param, mode: :pg)
108
+ end
109
+
110
+ # Fetch the sort fields from :sort query parameter.
111
+ # If none is given, default sort by `first_name ASC` and `last_name ASC NULLS FIRST`.
112
+ def sort_param
113
+ params[:sort].presence || "+first_name,+last_name"
114
+ end
115
+ ```
116
+
117
+ ### Error
118
+
119
+ | Class | Description |
120
+ | ----------- | ----------- |
121
+ | `SortParam::UnsupportedSortField` | Raised when a sort field from the parameter isn't included in the whitelisted sort fields. |
122
+
123
+ ## Development
124
+
125
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
126
+
127
+ To install this gem onto your local machine, run `bundle exec rake install`.
128
+
129
+ ## Contributing
130
+
131
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jsonb-uy/sort_param.
132
+
133
+ ## License
134
+
135
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,90 @@
1
+ module SortParam
2
+ class Definition
3
+ attr_reader :fields_hash
4
+
5
+ # Creates a new SortParam definition that whitelists the columns that are allowed to
6
+ # sorted (i.e. used in SQL ORDER BY).
7
+ def initialize
8
+ @fields_hash = {}
9
+ end
10
+
11
+ # Allows whitelisting columns using a block
12
+ #
13
+ # @param block [Proc] Field definition block
14
+ #
15
+ # @return [self] Definition instance
16
+ def define(&block)
17
+ raise ArgumentError.new("Missing block") unless block_given?
18
+
19
+ instance_eval(&block)
20
+
21
+ self
22
+ end
23
+
24
+ # Add a whitelisted column
25
+ #
26
+ # @param name [String, Symbol] column name
27
+ # @param defaults [Hash] column default options:
28
+ # * nulls (Symbol) nulls sort order. `:last` or `:first`
29
+ #
30
+ # @return [self] Definition instance
31
+ def field(name, defaults = {})
32
+ name = name.to_s
33
+ return if name.strip.empty?
34
+
35
+ fields_hash[name] = defaults
36
+
37
+ self
38
+ end
39
+
40
+ # Get default column options
41
+ #
42
+ # @param name [String] column name
43
+ #
44
+ # @return [Hash, NilClass] Default options
45
+ def field_defaults(name)
46
+ return nil if @fields_hash[name].nil?
47
+
48
+ @fields_hash[name].dup
49
+ end
50
+
51
+ # Parse then translate a sort string expression
52
+ #
53
+ # @param sort_string [String] Sort expression. Comma-separated sort fields.
54
+ # @param mode [Symbol, NilClass] Translation format
55
+ # * `:pg` for PostgreSQL ORDER BY SQL
56
+ # * `:mysql` for MySQL ORDER BY SQL
57
+ # * `:hash`/nil for the default hash representation.
58
+ #
59
+ # @example Sort by first_name ASC and then by last_name DESC
60
+ # definition.load!("+first_name,-last_name")
61
+ # # OR
62
+ # definition.load!("first_name,-last_name")
63
+ #
64
+ # @example Sort by first_name DESC NULLS LAST
65
+ # definition.load!("-first_name:nulls_last")
66
+ #
67
+ # @example Sort by first_name ASC NULLS FIRST
68
+ # definition.load!("+first_name:nulls_first")
69
+ #
70
+ # @return [Hash, String, NilClass] Translated to SQL or Hash.
71
+ # Returns nil if there is no column to sort.
72
+ #
73
+ def load!(sort_string, mode: :hash)
74
+ fields = Fields.new(sort_string)
75
+ validate_fields!(fields)
76
+
77
+ formatter = Formatters::Formatter.for(mode)
78
+ formatter.new(self).format(*fields)
79
+ end
80
+
81
+ private
82
+
83
+ def validate_fields!(fields)
84
+ unknown_field = (fields.names - fields_hash.keys).first
85
+ return true if unknown_field.nil?
86
+
87
+ raise SortParam::UnsupportedSortField.new("Unsupported sort field: #{unknown_field}")
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,54 @@
1
+ module SortParam
2
+ class Field
3
+ SORT_SYMBOL_DIRECTION = { "+" => :asc, "-" => :desc }.freeze
4
+
5
+ class << self
6
+ include Utilities
7
+
8
+ def from_string(sort_string)
9
+ return nil if blank?(sort_string)
10
+
11
+ name = column_name(sort_string)
12
+ return nil if blank?(name)
13
+
14
+ direction = sort_direction(sort_string)
15
+ nulls = nulls_order(sort_string)
16
+
17
+ Field.new(name, direction, nulls)
18
+ end
19
+
20
+ private
21
+
22
+ def sort_direction(str)
23
+ return :asc unless SORT_SYMBOL_DIRECTION[str[0]]
24
+
25
+ SORT_SYMBOL_DIRECTION[str[0]]
26
+ end
27
+
28
+ def column_name(str)
29
+ name = SORT_SYMBOL_DIRECTION[str[0]].nil? ? str : str.slice(1..-1)
30
+ name.strip!
31
+
32
+ return nil if blank?(name)
33
+ return name if nulls_order(name).nil?
34
+
35
+ name.sub(/(:nulls_last|:nulls_first)$/, "")
36
+ end
37
+
38
+ def nulls_order(str)
39
+ return :first if str.end_with?(":nulls_first")
40
+ return :last if str.end_with?(":nulls_last")
41
+
42
+ nil
43
+ end
44
+ end
45
+
46
+ attr_reader :name, :direction, :nulls
47
+
48
+ def initialize(name, direction, nulls = nil)
49
+ @name = name
50
+ @direction = direction
51
+ @nulls = nulls
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,50 @@
1
+ module SortParam
2
+ class Fields
3
+ include Enumerable
4
+ include Utilities
5
+
6
+ def initialize(sort_string = nil)
7
+ @fields = {}
8
+
9
+ return if blank?(sort_string)
10
+
11
+ parse_and_build_fields(sort_string)
12
+ end
13
+
14
+ def [](name)
15
+ fields[name]
16
+ end
17
+
18
+ def names
19
+ fields.keys
20
+ end
21
+
22
+ def <<(field)
23
+ fields[field.name] = field
24
+ end
25
+
26
+ def each(&block)
27
+ fields.values.each(&block)
28
+ end
29
+
30
+ def empty?
31
+ names.empty?
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :fields
37
+
38
+ def parse_and_build_fields(sort_string)
39
+ sort_string.split(",").each do |sort_token|
40
+ sort_token.strip!
41
+ field = Field.from_string(sort_token)
42
+ next if field.nil?
43
+
44
+ self << field
45
+ end
46
+
47
+ self
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SortParam
4
+ module Formatters
5
+ class Formatter
6
+ def self.for(mode)
7
+ return Formatters::PG if mode == :pg
8
+ return Formatters::MySQL if mode == :mysql
9
+
10
+ Formatters::Hash
11
+ end
12
+
13
+ def initialize(definition)
14
+ @definition = definition
15
+ end
16
+
17
+ def format(*fields)
18
+ return format_collection(fields) if fields.size > 1
19
+
20
+ field = fields[0]
21
+ return nil if field.nil?
22
+
23
+ format_field(field)
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :definition
29
+
30
+ def format_collection(fields)
31
+ raise NotImplementedError
32
+ end
33
+
34
+ def format_field(field)
35
+ raise NotImplementedError
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SortParam
4
+ module Formatters
5
+ class Hash < Formatter
6
+ private
7
+
8
+ def format_field(field)
9
+ field_data = definition.field_defaults(field.name) || {}
10
+ field_data.merge!(direction: field.direction)
11
+ field_data.merge!(nulls: field.nulls) unless field.nulls.nil?
12
+
13
+ { field.name => field_data }
14
+ end
15
+
16
+ def format_collection(fields)
17
+ fields.map { |field| format(field) }
18
+ .inject(&:merge!)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SortParam
4
+ module Formatters
5
+ class MySQL < Formatter
6
+ private
7
+
8
+ def format_collection(fields)
9
+ fields.map { |field| format(field) }.join(", ")
10
+ end
11
+
12
+ def format_field(field)
13
+ field_defaults = definition.field_defaults(field.name) || {}
14
+ column_name = field_defaults[:column_name] || field.name
15
+
16
+ nulls = (field.nulls || field_defaults[:nulls]).to_s
17
+ nulls_sort_order = nulls_order(column_name, nulls)
18
+ return "#{column_name} #{field.direction}" if nulls_sort_order.nil?
19
+
20
+ "#{nulls_sort_order}, #{column_name} #{field.direction}"
21
+ end
22
+
23
+ def nulls_order(column_name, nulls)
24
+ return "#{column_name} is not null" if nulls == "first"
25
+ return "#{column_name} is null" if nulls == "last"
26
+
27
+ nil
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SortParam
4
+ module Formatters
5
+ class PG < Formatter
6
+ private
7
+
8
+ def format_collection(fields)
9
+ fields.map { |field| format(field) }.join(", ")
10
+ end
11
+
12
+ def format_field(field)
13
+ field_defaults = definition.field_defaults(field.name) || {}
14
+ column_name = field_defaults[:column_name] || field.name
15
+
16
+ nulls = (field.nulls || field_defaults[:nulls]).to_s
17
+ "#{column_name} #{field.direction}#{nulls_order(nulls)}"
18
+ end
19
+
20
+ def nulls_order(nulls)
21
+ return " nulls first" if nulls == "first"
22
+ return " nulls last" if nulls == "last"
23
+
24
+ nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ module SortParam
2
+ module Utilities
3
+ def blank?(str)
4
+ return true if str.nil? || str == ""
5
+ return false unless str.is_a?(String)
6
+
7
+ str.strip!
8
+ str.empty?
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SortParam
4
+ VERSION = "0.1.0"
5
+ end
data/lib/sort_param.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sort_param/utilities"
4
+ require_relative "sort_param/formatters/formatter"
5
+ require_relative "sort_param/formatters/hash"
6
+ require_relative "sort_param/formatters/mysql"
7
+ require_relative "sort_param/formatters/pg"
8
+ require_relative "sort_param/field"
9
+ require_relative "sort_param/fields"
10
+ require_relative "sort_param/definition"
11
+ require_relative "sort_param/version"
12
+
13
+ module SortParam
14
+ class UnsupportedSortField < StandardError; end
15
+
16
+ # Creates a new SortParam definition that whitelists the columns that are allowed to
17
+ # sorted (i.e. used in SQL ORDER BY).
18
+ #
19
+ # @param block [Proc] Field definition block
20
+ #
21
+ # @example
22
+ # SortParam.define do
23
+ # field :first_name
24
+ # field :last_name, nulls: :last
25
+ # end
26
+ #
27
+ # @return [Definition] Sort param definition
28
+ #
29
+ def self.define(&block)
30
+ Definition.new.define(&block)
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sort_param
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Uy Jayson B
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-06-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Sort records using a sort query parameter à la JSON-API style
14
+ email:
15
+ - uy.json.dev@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - LICENSE
22
+ - README.md
23
+ - lib/sort_param.rb
24
+ - lib/sort_param/definition.rb
25
+ - lib/sort_param/field.rb
26
+ - lib/sort_param/fields.rb
27
+ - lib/sort_param/formatters/formatter.rb
28
+ - lib/sort_param/formatters/hash.rb
29
+ - lib/sort_param/formatters/mysql.rb
30
+ - lib/sort_param/formatters/pg.rb
31
+ - lib/sort_param/utilities.rb
32
+ - lib/sort_param/version.rb
33
+ homepage: https://github.com/jsonb-uy/sort_param
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ homepage_uri: https://github.com/jsonb-uy/sort_param
38
+ source_code_uri: https://github.com/jsonb-uy/sort_param
39
+ changelog_uri: https://github.com/jsonb-uy/sort_param/blob/main/CHANGELOG.md
40
+ documentation_uri: https://rubydoc.info/github/jsonb-uy/sort_param/main
41
+ bug_tracker_uri: https://github.com/jsonb-uy/sort_param/issues
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.3.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.4.6
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Sort records using a sort query parameter à la JSON-API style
61
+ test_files: []