ordy 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,188 @@
1
+ module Ordy
2
+ module Orm
3
+ module ActiveRecord
4
+ module Orderable
5
+
6
+ def self.included(base)
7
+ base.extend(Order::ClassMethods)
8
+ end
9
+
10
+ class Order
11
+ DELIMITER = '-'.freeze
12
+ include Enumerable
13
+
14
+ attr_reader :model, :default
15
+
16
+ # @param [ActiveRecord::Model] model
17
+ def initialize(model)
18
+ @model = model
19
+ @orderables = {}
20
+ end
21
+
22
+ def each(&block)
23
+ @orderables.each(&block)
24
+ end
25
+
26
+ def [](key)
27
+ @orderables.fetch(key)
28
+ end
29
+
30
+ # columns :name, :email
31
+ #
32
+ # @param [Array][Symbol] column_names
33
+ def columns(*column_names)
34
+ column_names.each do |column|
35
+ @orderables[column] = { args: { table: model.table_name, column: column },
36
+ orderable: Orderable::ByColumn }
37
+ end
38
+ end
39
+
40
+ # associations comment: :scripts
41
+ #
42
+ # @param [Array][Hash] associations
43
+ def associations(associations)
44
+ associations.each do |assoc, opts|
45
+ column, association = if opts.is_a?(Hash)
46
+ [opts.fetch(:column), opts.fetch(:as, assoc)]
47
+ else
48
+ [opts, assoc]
49
+ end
50
+ table_name = model.reflections.symbolize_keys.fetch(assoc).class_name.constantize.table_name
51
+
52
+ @orderables[association] = { args: { association: association,
53
+ table: table_name,
54
+ column: column },
55
+ orderable: Orderable::ByAssociation }
56
+ end
57
+ end
58
+
59
+ # specified(state: %w(new pending_migration migrating failed))
60
+ #
61
+ # @param [Array][Hash] args
62
+ def specified(args)
63
+ args.each do |column, values|
64
+ specified_column = "specified_#{column}".to_sym
65
+
66
+ @orderables[specified_column] = { args: { table: model.table_name,
67
+ column: column,
68
+ values: values },
69
+ orderable: Orderable::BySpecified }
70
+ end
71
+ end
72
+
73
+ # query :users do |scope, args|
74
+ # scope.where(...).order(field: args[:direction])
75
+ # end
76
+ #
77
+ # @param [Symbol] query
78
+ # @param [Proc] block
79
+ def query(query, &block)
80
+ @orderables[query] = { args: {}, orderable: block }
81
+ end
82
+
83
+ # default do
84
+ # order_by_specified(:state).order_by(started: :desc, created_at: :desc)
85
+ # end
86
+ #
87
+ # @param [Symbol] name
88
+ # @param [Proc] block
89
+ def default(name = nil, &block)
90
+ return -> { order_by(name => :asc) } if name.present?
91
+ @default = block if block_given?
92
+ @default ||= -> { order(nil) }
93
+ end
94
+
95
+ module ClassMethods
96
+
97
+ # orderable_by do
98
+ # column :name
99
+ # association :comments
100
+ # specified(state: %w(new pending_migration migrating failed))
101
+ #
102
+ # query(:topics) do |scope, args|
103
+ # ...
104
+ # end
105
+ # end
106
+ #
107
+ # @param [Proc] block
108
+ def orderable_by(&block)
109
+ @_orderables ||= Order.new(self)
110
+ @_orderables.instance_eval(&block)
111
+ @_orderables
112
+ end
113
+
114
+ # Model.order_by(name: :desc)
115
+ #
116
+ # Model.order_by('name-desc')
117
+ #
118
+ # Default direction :asc
119
+ # Model.order_by(name)
120
+ #
121
+ # Call default do method or order(nil)
122
+ # Model.order_by(nil)
123
+ # Model.order_by('')
124
+ #
125
+ # @param [Hash] order_query
126
+ def order_by(order_query = nil)
127
+ return default_order if order_query.nil? || order_query.blank?
128
+ return default_order(order_query) if order_query.is_a?(Symbol)
129
+
130
+
131
+ specs = case order_query
132
+ when String then parse_order_query(order_query)
133
+ when Hash then parse_order_hash(order_query)
134
+ else
135
+ []
136
+ end
137
+
138
+ return default_order if specs.blank?
139
+
140
+ scope = default_scope
141
+
142
+ specs.each do |spec|
143
+ orderable = @_orderables[spec[:orderable]]
144
+ orderable[:args][:direction] = spec[:direction]
145
+
146
+ scope = orderable[:orderable].call(scope, orderable[:args])
147
+ end
148
+
149
+ scope
150
+ end
151
+
152
+ # Model.order_by_specified(:name)
153
+ #
154
+ # @param [Symbol] name
155
+ def order_by_specified(name)
156
+ orderable = @_orderables["specified_#{name}".to_sym]
157
+ orderable[:orderable].call(default_scope, orderable[:args])
158
+ end
159
+
160
+ def default_order(name = nil)
161
+ default_scope.instance_exec(&@_orderables.default(name))
162
+ end
163
+
164
+ private
165
+
166
+ # @param [Hash] order_query
167
+ # # @return [Array][Hash]
168
+ def parse_order_hash(order_query)
169
+ order_query.symbolize_keys.map { |(orderable, direction)| { orderable: orderable, direction: direction } }
170
+ end
171
+
172
+ # @param [String] order_query
173
+ # @return [Array][Hash]
174
+ def parse_order_query(order_query)
175
+ orderable, direction = order_query.to_s.split(DELIMITER).map(&:to_s).map(&:strip)
176
+ direction = 'asc' unless %w(desc asc).include?(direction)
177
+ [{ orderable: orderable.to_sym, direction: direction.to_sym }]
178
+ end
179
+
180
+ def default_scope
181
+ current_scope || where(nil)
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
data/lib/ordy.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Ordy
2
+ end
3
+
4
+ require_relative 'config/settings'
5
+ require_relative 'ordy/orm/active_record/orderable'
6
+ require_relative 'ordy/orm/active_record/orderable/by_association'
7
+ require_relative 'ordy/orm/active_record/orderable/by_column'
8
+ require_relative 'ordy/orm/active_record/orderable/by_specified'
9
+ require_relative 'ordy/helpers/action_view/orderable_link_helper'
data/ordy.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'ordy'
7
+ spec.version = File.read('VERSION')
8
+ spec.date = '2018-11-23'
9
+ spec.summary = 'Simple sorting gem'
10
+ spec.description = 'Simple sorting gem for RubyObject\'s and ORM\'s '
11
+ spec.authors = ['nine.ch Development-Team']
12
+ spec.email = 'development@nine.ch'
13
+ spec.files = ['lib/ordy.rb']
14
+ spec.homepage = 'http://github.com/ninech/'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files`.split("\n")
18
+ spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ spec.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.required_ruby_version = '>= 2.0.0'
23
+
24
+ spec.add_development_dependency 'bundler'
25
+ spec.add_development_dependency 'rspec'
26
+ spec.add_development_dependency 'pry'
27
+ spec.add_development_dependency 'sqlite3'
28
+ spec.add_development_dependency 'appraisal'
29
+
30
+ spec.add_runtime_dependency 'activerecord', '>= 4.0.0'
31
+ spec.add_runtime_dependency 'actionview', '>= 4.0.0'
32
+ end
data/readme.md ADDED
@@ -0,0 +1,138 @@
1
+ [![Build Status](https://travis-ci.org/ninech/ordy.svg)](https://travis-ci.org/ninech/ordy)
2
+
3
+ ## Usage
4
+
5
+ #### Ordering
6
+
7
+ __Model usage__
8
+
9
+ ```rb
10
+ class User < ApplicationRecord
11
+ include Ordy::Orm::ActiveRecord::Orderable
12
+
13
+ has_many :comments
14
+
15
+ orderable_by do
16
+ # order by field
17
+ columns :name, :email
18
+
19
+ # order by relations field
20
+ associations comments: :scripts
21
+
22
+ # sort by specified order
23
+ specified(state: %w(new, pending, active, removed))
24
+
25
+ # custom query
26
+ query :custom_query do |scope, args|
27
+ scope.where(name: 'example').order(email: args[:direction])
28
+ end
29
+ end
30
+ end
31
+ ```
32
+
33
+ __Code usage__
34
+
35
+ ```rb
36
+ # order by columns
37
+ User.order_by(name: :asc)
38
+ # or
39
+ User.order_by('name-asc')
40
+
41
+ # order by multiple columns
42
+ User.order_by(name: :asc, email: :desc)
43
+ # or
44
+ User.order_by(name: :asc).order_by(email: :desc)
45
+
46
+ # order by association column
47
+ User.order_by(comments: :asc)
48
+
49
+ # order by specified column values
50
+ User.order_by_specified(:state)
51
+
52
+ # order by custom query
53
+ User.order_by(custom_query: :asc)
54
+
55
+ # default ordering
56
+ User.order_by('')
57
+ # or
58
+ User.order_by(nil)
59
+
60
+ # ordering by more than one criteria
61
+ User.order_by(email: :asc, state: :desc)
62
+ ```
63
+
64
+ #### View helper
65
+
66
+ __Helper inclusion__
67
+
68
+ ```rb
69
+ # users_helper.rb
70
+
71
+ include Ordy::Helpers::ActionView::OrderableLinkHelper
72
+ ```
73
+
74
+ __Html usage__
75
+
76
+ ```html
77
+ # index.html
78
+
79
+ <%= order_link('link_title', 'ordering_filed') %>
80
+ ```
81
+
82
+ __Controller usage__
83
+
84
+ ```rb
85
+ class UsersController
86
+ def index
87
+ if params[:order_by].present? && params[:direction].present?
88
+ @users = User.order_by(params[:order_by] => params[:direction])
89
+ else
90
+ @users = User.all
91
+ end
92
+ end
93
+ end
94
+ ```
95
+
96
+ ## Run development environment
97
+
98
+ ```bash
99
+ # position in gem dir
100
+ cd ordy
101
+
102
+ # build app
103
+ docker-compose build app
104
+
105
+ # run app and attach with bash
106
+ docker-compose run app /bin/bash
107
+ ```
108
+
109
+ ## Test
110
+
111
+ ```bash
112
+ # install dependencies
113
+ bundle exec appraisal install
114
+
115
+ # run tests for rails 4 env
116
+ bundle exec appraisal rails-4 rspec
117
+
118
+ # run tests for rails 5 env
119
+ bundle exec appraisal rails-5 rspec
120
+ ```
121
+
122
+ # Contributing
123
+ Bug reports and pull requests are very welcome [on GitHub](https://github.com/ninech/ordy).
124
+
125
+ Before opening a PR, please
126
+
127
+ extend the existing specs
128
+ - run rspec
129
+ - run rubocop and fix your warnings
130
+ - check if this README.md file needs adjustments
131
+
132
+ #License
133
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
134
+
135
+ # About
136
+ This gem is currently maintained and funded by [nine](https://www.nine.ch/de/home).
137
+
138
+ [![logo of the company 'nine'](https://logo.apps.at-nine.ch/Dmqied_eSaoBMQwk3vVgn4UIgDo=/trim/500x0/logo_claim.png)](https://www.nine.ch)
@@ -0,0 +1,151 @@
1
+ require 'spec_helper'
2
+ require './spec/support/orm/active_record/active_record_test_db'
3
+
4
+ describe Ordy::Orm::ActiveRecord::Orderable do
5
+
6
+ describe '#order_by' do
7
+
8
+ context 'columns' do
9
+ let(:base_query) { User }
10
+ let(:order) { base_query.order_by(order_by) }
11
+ let(:result) { order.pluck(:name) }
12
+
13
+ context 'order by name: :asc' do
14
+ let(:order_by) { { name: :asc } }
15
+ specify { expect(result).to eq(%w(axample demo example)) }
16
+ end
17
+
18
+ context 'order by name-asc' do
19
+ let(:order_by) { 'name-asc' }
20
+ specify { expect(result).to eq(%w(axample demo example)) }
21
+ end
22
+
23
+ context 'order by name: :desc' do
24
+ let(:order_by) { { name: :desc } }
25
+ specify { expect(result).to eq(%w(example demo axample)) }
26
+ end
27
+
28
+ context 'order by name-desc' do
29
+ let(:order_by) { 'name-desc' }
30
+ specify { expect(result).to eq(%w(example demo axample)) }
31
+ end
32
+
33
+ context 'base query immutability' do
34
+ it 'should not change base query' do
35
+ query = base_query.where(name: 'example')
36
+ query.order_by(name: :desc).pluck(:name)
37
+ expect(query.to_sql).to eq('SELECT "users".* FROM "users" WHERE "users"."name" = \'example\'')
38
+ end
39
+ end
40
+ end
41
+
42
+ context 'associations' do
43
+ let(:base_query) { User }
44
+ let(:order) { base_query.order_by(order_by) }
45
+ let(:result) { order.pluck(:id) }
46
+
47
+ context 'order by name: :asc' do
48
+ let(:order_by) { { comments: :asc } }
49
+ specify { expect(result).to eq([3, 2, 1]) }
50
+ end
51
+
52
+ context 'order by name-asc' do
53
+ let(:order_by) { 'comments-asc' }
54
+ specify { expect(result).to eq([3, 2, 1]) }
55
+ end
56
+
57
+ context 'order by name: :desc' do
58
+ let(:order_by) { { comments: :desc } }
59
+ specify { expect(result).to eq([1, 2, 3]) }
60
+ end
61
+
62
+ context 'order by name-desc' do
63
+ let(:order_by) { 'comments-desc' }
64
+ specify { expect(result).to eq([1, 2, 3]) }
65
+ end
66
+
67
+ context 'base query immutability' do
68
+ it 'should not change base query' do
69
+ query = base_query.where(name: 'example')
70
+ query.order_by(comments: :desc).pluck(:id)
71
+ expect(query.to_sql).to eq('SELECT "users".* FROM "users" WHERE "users"."name" = \'example\'')
72
+ end
73
+ end
74
+ end
75
+
76
+ context 'query' do
77
+ let(:base_query) { User }
78
+ let(:order) { base_query.order_by(order_by) }
79
+ let(:result) { order.pluck(:email) }
80
+
81
+ context 'order by name: :asc' do
82
+ let(:order_by) { { custom_query: :asc } }
83
+ specify { expect(result).to eq(%w(example@example.com custom_example@example.com)) }
84
+ end
85
+
86
+ context 'order by name-asc' do
87
+ let(:order_by) { 'custom_query-asc' }
88
+ specify { expect(result).to eq(%w(example@example.com custom_example@example.com)) }
89
+ end
90
+
91
+ context 'order by name: :desc' do
92
+ let(:order_by) { { custom_query: :desc } }
93
+ specify { expect(result).to eq(%w(custom_example@example.com example@example.com)) }
94
+ end
95
+
96
+ context 'order by name-desc' do
97
+ let(:order_by) { 'custom_query-desc' }
98
+ specify { expect(result).to eq(%w(custom_example@example.com example@example.com)) }
99
+ end
100
+
101
+ context 'base query immutability' do
102
+ it 'should not change base query' do
103
+ query = base_query.where(name: 'example')
104
+ query.order_by('custom_query-desc').pluck(:email)
105
+ expect(query.to_sql).to eq('SELECT "users".* FROM "users" WHERE "users"."name" = \'example\'')
106
+ end
107
+ end
108
+ end
109
+
110
+ context 'default ordering' do
111
+ let(:spacing) { ENV['BUNDLE_GEMFILE'].include?('gemfiles/rails_4.gemfile') ? ' ' : ' ' }
112
+
113
+ context 'default proc' do
114
+ let(:query) { User.where(name: 'example') }
115
+
116
+ it 'should return asc ordering for nil' do
117
+ expect(query.order_by(nil).to_sql).to eq('SELECT "users".* FROM "users" WHERE "users"."name" = \'example\'' + spacing + 'ORDER BY "users"."state"=\'active\' DESC,"users"."state"=\'pending\' DESC, users.name asc')
118
+ end
119
+
120
+ it 'should not mutate base query' do
121
+ expect(query.to_sql).to eq('SELECT "users".* FROM "users" WHERE "users"."name" = \'example\'')
122
+ end
123
+ end
124
+
125
+ context 'default field direction' do
126
+ let(:query) { User.where(name: 'example') }
127
+
128
+ it 'should return asc ordering for :field' do
129
+ expect(query.order_by(:name).to_sql).to eq('SELECT "users".* FROM "users" WHERE "users"."name" = \'example\'' + spacing + 'ORDER BY users.name asc')
130
+ end
131
+
132
+ it 'should not mutate base query' do
133
+ expect(query.to_sql).to eq('SELECT "users".* FROM "users" WHERE "users"."name" = \'example\'')
134
+ end
135
+ end
136
+
137
+ context 'default ordering' do
138
+ let(:base_query_sql) { 'SELECT "comments".* FROM "comments" WHERE "comments"."user_id" = 1' }
139
+ let(:query) { Comment.where(user_id: 1) }
140
+
141
+ it 'should return asc ordering for \'\'' do
142
+ expect(query.order_by('').to_sql).to eq(base_query_sql)
143
+ end
144
+
145
+ it 'should not mutate base query' do
146
+ expect(query.to_sql).to eq(base_query_sql)
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,5 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ require 'ordy'
5
+ require 'pry'
@@ -0,0 +1,95 @@
1
+ require 'active_record'
2
+
3
+ RSpec.configure do |config|
4
+ config.before(:all) do
5
+ create_db
6
+ migrate
7
+ seed
8
+ end
9
+
10
+ config.after(:all) do
11
+ drop_db
12
+ end
13
+ end
14
+
15
+ def create_db
16
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
17
+ end
18
+
19
+ def migrate
20
+ ActiveRecord::Base.connection.create_table :users do |t|
21
+ t.text :name
22
+ t.text :email
23
+ t.text :state
24
+ end
25
+
26
+ ActiveRecord::Base.connection.create_table :comments do |t|
27
+ t.integer :user_id
28
+ t.text :content
29
+ end
30
+ end
31
+
32
+ def seeds
33
+ [
34
+ { table: "'users'",
35
+ fields: %w(id name email state),
36
+ values: [
37
+ [1, "'demo'", "'demo@demo.com'", "'active'"],
38
+ [2, "'axample'", "'example@example.com'", "'pending'"],
39
+ [3, "'example'", "'custom_example@example.com'", "'active'"]
40
+ ]
41
+ },
42
+ { table: "'comments'",
43
+ fields: %w(content user_id),
44
+ values: [
45
+ ["'example'", 1],
46
+ ["'demo'", 2]
47
+ ]
48
+ }
49
+ ]
50
+ end
51
+
52
+ def seed
53
+ seeds.each do |model|
54
+ model[:values].each do |args|
55
+ ActiveRecord::Base.connection.execute("INSERT INTO #{model[:table]} (#{model[:fields].join(',')}) VALUES (#{args.join(',')})")
56
+ end
57
+ end
58
+ end
59
+
60
+ def drop_db
61
+ [:users, :comments].each do |table|
62
+ ActiveRecord::Base.connection.drop_table table
63
+ end
64
+ end
65
+
66
+ class Comment < ActiveRecord::Base
67
+ include Ordy::Orm::ActiveRecord::Orderable
68
+
69
+ orderable_by do
70
+
71
+ end
72
+
73
+ belongs_to :user
74
+ end
75
+
76
+ class User < ActiveRecord::Base
77
+ include Ordy::Orm::ActiveRecord::Orderable
78
+
79
+ has_many :comments
80
+
81
+ orderable_by do
82
+ columns :name, :email
83
+ associations comments: :content
84
+ specified(state: %w(active pending))
85
+
86
+ query :custom_query do |scope, args|
87
+ scope.where('email LIKE \'%example%\'',).order(id: args.fetch(:direction))
88
+ end
89
+
90
+ default do
91
+ order_by_specified(:state).order_by(:name)
92
+ end
93
+ end
94
+ end
95
+