ordy 1.1.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.
@@ -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
+