mincer 0.1.2 → 0.2.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/.travis.yml +3 -0
- data/Gemfile +2 -1
- data/README.md +111 -64
- data/lib/mincer/action_view/sort_helper.rb +7 -7
- data/lib/mincer/base.rb +1 -1
- data/lib/mincer/config.rb +29 -0
- data/lib/mincer/core_ext/string.rb +5 -0
- data/lib/mincer/processors/cache_digest/processor.rb +54 -0
- data/lib/mincer/processors/helpers.rb +15 -0
- data/lib/mincer/processors/pagination/processor.rb +63 -0
- data/lib/mincer/processors/pg_json_dumper/processor.rb +69 -0
- data/lib/mincer/processors/pg_search/processor.rb +148 -0
- data/lib/mincer/processors/pg_search/sanitizer.rb +61 -0
- data/lib/mincer/processors/pg_search/search_engines/array.rb +41 -0
- data/lib/mincer/processors/pg_search/search_engines/base.rb +56 -0
- data/lib/mincer/processors/pg_search/search_engines/fulltext.rb +58 -0
- data/lib/mincer/processors/pg_search/search_engines/trigram.rb +41 -0
- data/lib/mincer/processors/pg_search/search_statement.rb +31 -0
- data/lib/mincer/processors/sorting/processor.rb +113 -0
- data/lib/mincer/version.rb +1 -1
- data/lib/mincer.rb +31 -31
- data/mincer.gemspec +0 -2
- data/spec/lib/mincer/action_view/sort_helper_spec.rb +11 -0
- data/spec/lib/mincer/base_spec.rb +15 -0
- data/spec/lib/mincer/config_spec.rb +7 -0
- data/spec/lib/{processors/cache_digest_spec.rb → mincer/processors/cache_digest/processor_spec.rb} +2 -9
- data/spec/lib/{processors/paginate_spec.rb → mincer/processors/pagination/processor_spec.rb} +43 -17
- data/spec/lib/{processors/pg_json_dumper_spec.rb → mincer/processors/pg_json_dumper/processor_spec.rb} +2 -6
- data/spec/lib/mincer/processors/pg_search/processor_spec.rb +268 -0
- data/spec/lib/mincer/processors/pg_search/sanitizer_spec.rb +38 -0
- data/spec/lib/mincer/processors/pg_search/search_engines/array_spec.rb +83 -0
- data/spec/lib/mincer/processors/pg_search/search_engines/fulltext_spec.rb +101 -0
- data/spec/lib/mincer/processors/pg_search/search_engines/trigram_spec.rb +91 -0
- data/spec/lib/mincer/processors/sorting/processor_spec.rb +181 -0
- data/spec/mincer_config.rb +38 -0
- data/spec/spec_helper.rb +40 -4
- data/spec/support/postgres_adapter.rb +12 -3
- data/spec/support/sqlite3_adapter.rb +3 -0
- metadata +42 -45
- data/lib/mincer/processors/cache_digest.rb +0 -50
- data/lib/mincer/processors/paginate.rb +0 -41
- data/lib/mincer/processors/pg_json_dumper.rb +0 -51
- data/lib/mincer/processors/search.rb +0 -34
- data/lib/mincer/processors/sort.rb +0 -59
- data/spec/lib/processors/search_spec.rb +0 -77
- data/spec/lib/processors/sort_spec.rb +0 -77
@@ -0,0 +1,148 @@
|
|
1
|
+
# This and all processors are heavily influenced by pg_search(https://github.com/Casecommons/pg_search)
|
2
|
+
module Mincer
|
3
|
+
module Processors
|
4
|
+
module PgSearch
|
5
|
+
class Processor
|
6
|
+
include ::Mincer::Processors::Helpers
|
7
|
+
|
8
|
+
def initialize(mincer)
|
9
|
+
@mincer, @args, @relation = mincer, mincer.args, mincer.relation
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply
|
13
|
+
if Mincer.postgres?
|
14
|
+
@relation = apply_pg_search(@relation, @args)
|
15
|
+
else
|
16
|
+
@relation
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def apply_pg_search(relation, args)
|
21
|
+
relation.where(conditions(args)).reorder(rank(args))
|
22
|
+
end
|
23
|
+
|
24
|
+
def conditions(args)
|
25
|
+
search_statements_conditions = search_statements.map do |search_statement|
|
26
|
+
conditions = pg_search_engines(args, search_statement).map do |pg_search_engine|
|
27
|
+
pg_search_engine.conditions
|
28
|
+
end.compact
|
29
|
+
join_expressions(conditions, options[:join_with] || :or)
|
30
|
+
end.compact
|
31
|
+
join_expressions(search_statements_conditions, options[:join_with] || :or).try(:to_sql)
|
32
|
+
end
|
33
|
+
|
34
|
+
def rank(args)
|
35
|
+
search_statements_conditions = search_statements.map do |search_statement|
|
36
|
+
conditions = pg_search_engines(args, search_statement).map do |pg_search_engine|
|
37
|
+
pg_search_engine.rank
|
38
|
+
end.compact
|
39
|
+
join_expressions(conditions, :+)
|
40
|
+
end.compact
|
41
|
+
rank = join_expressions(search_statements_conditions, :+).try(:to_sql)
|
42
|
+
"#{rank} DESC" if rank.present?
|
43
|
+
end
|
44
|
+
|
45
|
+
def pg_search_engines(args, search_statement)
|
46
|
+
Mincer.config.pg_search.engines.map do |engine_class|
|
47
|
+
engine_class.new(args, [search_statement])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def search_statements
|
52
|
+
@search_statements ||= params.any? { |param| param[:columns] } ? search_statements_from_params : default_search_statements
|
53
|
+
end
|
54
|
+
|
55
|
+
def search_statements_from_params
|
56
|
+
params.map do |param|
|
57
|
+
par = param.dup
|
58
|
+
SearchStatement.new(par.delete(:columns), search_statement_default_options(param[:engines]).merge(par))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# We use only text/string columns and avoid array
|
63
|
+
def default_search_statements
|
64
|
+
column_names = @relation.columns.reject do |column|
|
65
|
+
![:string, :text].include?(column.type) || column.array
|
66
|
+
end.map do |column|
|
67
|
+
"#{@relation.table_name}.#{column.name}"
|
68
|
+
end
|
69
|
+
[SearchStatement.new(column_names, search_statement_default_options([:fulltext]).merge(engines: [:fulltext]))]
|
70
|
+
end
|
71
|
+
|
72
|
+
def params
|
73
|
+
Array.wrap(@mincer.send(:pg_search_params))
|
74
|
+
end
|
75
|
+
|
76
|
+
def options
|
77
|
+
@mincer.send(:pg_search_options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def search_statement_default_options(engines)
|
81
|
+
(engines & [:fulltext, :trigram, :array]).inject({}) do |options, engine|
|
82
|
+
options = Mincer.config.pg_search.send("#{engine}_engine").merge(options)
|
83
|
+
options
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
module Options
|
90
|
+
extend ActiveSupport::Concern
|
91
|
+
|
92
|
+
def sort_by_rank
|
93
|
+
end
|
94
|
+
|
95
|
+
module ClassMethods
|
96
|
+
def skip_pg_search!
|
97
|
+
active_processors.delete(Mincer::Processors::PgSearch::Processor)
|
98
|
+
end
|
99
|
+
|
100
|
+
def skip_search!
|
101
|
+
skip_pg_search!
|
102
|
+
end
|
103
|
+
|
104
|
+
def pg_search(params, options = {})
|
105
|
+
class_eval <<-OPTIONS, __FILE__, __LINE__
|
106
|
+
def pg_search_params
|
107
|
+
@pg_search_params ||= #{params.inspect}
|
108
|
+
end
|
109
|
+
def pg_search_options
|
110
|
+
@pg_search_options ||= #{options.inspect}
|
111
|
+
end
|
112
|
+
OPTIONS
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def pg_search_params
|
117
|
+
@pg_search_params ||= {}
|
118
|
+
end
|
119
|
+
|
120
|
+
def pg_search_options
|
121
|
+
@pg_search_options ||= {}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Configuration
|
126
|
+
include ActiveSupport::Configurable
|
127
|
+
config_accessor :param_name do
|
128
|
+
'pattern'
|
129
|
+
end
|
130
|
+
config_accessor :fulltext_engine do
|
131
|
+
{ ignore_accent: true, any_word: false, dictionary: :simple, ignore_case: false }
|
132
|
+
end
|
133
|
+
config_accessor :trigram_engine do
|
134
|
+
{ ignore_accent: true, threshold: 0.3 }
|
135
|
+
end
|
136
|
+
config_accessor :array_engine do
|
137
|
+
{ ignore_accent: true, any_word: true }
|
138
|
+
end
|
139
|
+
config_accessor :engines do
|
140
|
+
[Mincer::PgSearch::SearchEngines::Fulltext, Mincer::PgSearch::SearchEngines::Array, Mincer::PgSearch::SearchEngines::Trigram]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
::Mincer.add_processor(:pg_search)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Mincer
|
2
|
+
module Processors
|
3
|
+
module PgSearch
|
4
|
+
class Sanitizer
|
5
|
+
AVAILABLE_SANITIZERS = [:coalesce, :ignore_case, :ignore_accent]
|
6
|
+
attr_accessor :term, :sanitizers
|
7
|
+
|
8
|
+
def initialize(term, *sanitizers)
|
9
|
+
@term, @sanitizers = term, AVAILABLE_SANITIZERS & Array.wrap(sanitizers).flatten
|
10
|
+
end
|
11
|
+
|
12
|
+
def sanitize_column
|
13
|
+
@sanitized_column ||= sanitize(Arel.sql(@term))
|
14
|
+
end
|
15
|
+
|
16
|
+
def sanitize_string(options = {})
|
17
|
+
if sanitizers.empty?
|
18
|
+
return options[:quote] ? Mincer.connection.quote(@term) : @term
|
19
|
+
end
|
20
|
+
@sanitized_string ||= sanitize(@term)
|
21
|
+
end
|
22
|
+
|
23
|
+
def sanitize(node)
|
24
|
+
sanitizers.inject(node) do |query, sanitizer|
|
25
|
+
query = self.class.send(sanitizer, query)
|
26
|
+
query
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.sanitize_column(term, *sanitizers)
|
31
|
+
new(term, *sanitizers).sanitize_column
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.sanitize_string(term, *sanitizers)
|
35
|
+
new(term, *sanitizers).sanitize_string
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.sanitize_string_quoted(term, *sanitizers)
|
39
|
+
new(term, *sanitizers).sanitize_string(quote: true)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.ignore_case(term)
|
43
|
+
Arel::Nodes::NamedFunction.new('lower', [term])
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.ignore_accent(term)
|
47
|
+
Arel::Nodes::NamedFunction.new('unaccent', [term])
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.coalesce(term, val = '')
|
51
|
+
if Mincer.pg_extension_installed?(:unaccent)
|
52
|
+
Arel::Nodes::NamedFunction.new('coalesce', [term, val])
|
53
|
+
else
|
54
|
+
term
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Mincer
|
2
|
+
module PgSearch
|
3
|
+
module SearchEngines
|
4
|
+
class Array < Base
|
5
|
+
|
6
|
+
def conditions
|
7
|
+
return nil unless prepared_search_statements.any?
|
8
|
+
arel_group do
|
9
|
+
conditions = prepared_search_statements.map do |search_statement|
|
10
|
+
if search_statement.pattern = args[search_statement.param_name]
|
11
|
+
terms_delimiter = search_statement.options[:any_word] ? '&&' : '@>'
|
12
|
+
arel_group(Arel::Nodes::InfixOperation.new(terms_delimiter, document_for(search_statement), query_for(search_statement)))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
join_expressions(conditions, :or)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def document_for(search_statement)
|
22
|
+
arel_group do
|
23
|
+
documents = search_statement.columns.map do |search_column|
|
24
|
+
Arel.sql(search_column + '::text[]')
|
25
|
+
end
|
26
|
+
join_expressions(documents, '||')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def query_for(search_statement)
|
31
|
+
normalized_pattern = search_statement.pattern.split(%r{\s|,}).uniq.reject(&:empty?).map do |item|
|
32
|
+
sanitize_string_quoted(item, search_statement.sanitizers).to_sql
|
33
|
+
end.join(',')
|
34
|
+
Arel.sql("ARRAY[#{normalized_pattern}]")
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Mincer
|
2
|
+
module PgSearch
|
3
|
+
module SearchEngines
|
4
|
+
class Base
|
5
|
+
include ::Mincer::Processors::Helpers
|
6
|
+
attr_reader :args, :search_statements
|
7
|
+
|
8
|
+
def initialize(args, search_statements)
|
9
|
+
@args, @search_statements = ::ActiveSupport::HashWithIndifferentAccess.new(args), search_statements
|
10
|
+
end
|
11
|
+
|
12
|
+
def arel_group(sql_string = nil)
|
13
|
+
sql_string = yield if block_given?
|
14
|
+
arel_query = sql_string.is_a?(String) ? Arel.sql(sql_string) : sql_string
|
15
|
+
Arel::Nodes::Grouping.new(arel_query)
|
16
|
+
end
|
17
|
+
|
18
|
+
def sanitize_column(term, sanitizers)
|
19
|
+
::Mincer::Processors::PgSearch::Sanitizer.sanitize_column(term, sanitizers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def sanitize_string(term, sanitizers)
|
23
|
+
::Mincer::Processors::PgSearch::Sanitizer.sanitize_string(term, sanitizers)
|
24
|
+
end
|
25
|
+
|
26
|
+
def sanitize_string_quoted(term, sanitizers)
|
27
|
+
::Mincer::Processors::PgSearch::Sanitizer.sanitize_string_quoted(term, sanitizers)
|
28
|
+
end
|
29
|
+
|
30
|
+
def search_engine_statements
|
31
|
+
@search_engine_statements ||= self.search_statements.select do |search_statement|
|
32
|
+
search_statement.options[:engines].try(:include?, engine_sym)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def prepared_search_statements
|
37
|
+
@prepared_search_statements ||= search_engine_statements.map do |search_statement|
|
38
|
+
search_statement.pattern = args[search_statement.param_name]
|
39
|
+
search_statement.pattern.present? ? search_statement : nil
|
40
|
+
end.compact
|
41
|
+
end
|
42
|
+
|
43
|
+
# Redefine this method in subclass if your engine name does not match class
|
44
|
+
def engine_sym
|
45
|
+
@engine_sym ||= self.class.name.to_s.demodulize.underscore.to_sym
|
46
|
+
end
|
47
|
+
|
48
|
+
def rank
|
49
|
+
#Must be implemented in subclasses
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Mincer
|
2
|
+
module PgSearch
|
3
|
+
module SearchEngines
|
4
|
+
class Fulltext < Base
|
5
|
+
DISALLOWED_TSQUERY_CHARACTERS = /[!(:&|)'?\\]/
|
6
|
+
|
7
|
+
def conditions
|
8
|
+
return nil unless prepared_search_statements.any?
|
9
|
+
arel_group do
|
10
|
+
conditions = prepared_search_statements.map do |search_statement|
|
11
|
+
arel_group(Arel::Nodes::InfixOperation.new('@@', document_for(search_statement), query_for(search_statement)))
|
12
|
+
end
|
13
|
+
join_expressions(conditions, :or)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def rank
|
18
|
+
return nil unless prepared_search_statements.any?
|
19
|
+
arel_group do
|
20
|
+
ranks = prepared_search_statements.map do |search_statement|
|
21
|
+
Arel::Nodes::NamedFunction.new('ts_rank', [document_for(search_statement), query_for(search_statement)]).to_sql
|
22
|
+
end
|
23
|
+
join_expressions(ranks, '+')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def prepared_search_statements
|
30
|
+
@prepared_search_statements ||= search_engine_statements.map do |search_statement|
|
31
|
+
pattern = args[search_statement.param_name]
|
32
|
+
search_statement.pattern = pattern && pattern.gsub(DISALLOWED_TSQUERY_CHARACTERS, ' ').split(' ').compact
|
33
|
+
search_statement.pattern.present? && search_statement.pattern.any? ? search_statement : nil
|
34
|
+
end.compact
|
35
|
+
end
|
36
|
+
|
37
|
+
def document_for(search_statement)
|
38
|
+
arel_group do
|
39
|
+
search_statement.columns.map do |search_column|
|
40
|
+
sanitized_term = sanitize_column(search_column, search_statement.sanitizers + [:coalesce])
|
41
|
+
Arel::Nodes::NamedFunction.new('to_tsvector', [search_statement.dictionary, sanitized_term]).to_sql
|
42
|
+
end.join(' || ')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def query_for(search_statement)
|
47
|
+
terms_delimiter = search_statement.options[:any_word] ? '|' : '&'
|
48
|
+
tsquery_sql = Arel.sql(search_statement.terms.map { |term| sanitize_string_quoted(term, search_statement.sanitizers).to_sql }.join(" || ' #{terms_delimiter} ' || "))
|
49
|
+
arel_group do
|
50
|
+
Arel::Nodes::NamedFunction.new('to_tsquery', [search_statement.dictionary, tsquery_sql])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Mincer
|
2
|
+
module PgSearch
|
3
|
+
module SearchEngines
|
4
|
+
class Trigram < Base
|
5
|
+
|
6
|
+
def conditions
|
7
|
+
return nil unless prepared_search_statements.any?
|
8
|
+
arel_group do
|
9
|
+
join_expressions(prepared_search_statements.map { |search_statement| document_for(search_statement) }, :or)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def rank
|
14
|
+
return nil unless prepared_search_statements.any?
|
15
|
+
arel_group do
|
16
|
+
join_expressions(prepared_search_statements.map { |search_statement| rank_for(search_statement) }, :+)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
|
23
|
+
def document_for(search_statement)
|
24
|
+
documents = search_statement.columns.map do |search_column|
|
25
|
+
similarity = Arel::Nodes::NamedFunction.new('similarity', [sanitize_column(search_column, search_statement.sanitizers), sanitize_string(search_statement.pattern, search_statement.sanitizers)])
|
26
|
+
arel_group(similarity.gteq(search_statement.threshold))
|
27
|
+
end
|
28
|
+
join_expressions(documents, :or)
|
29
|
+
end
|
30
|
+
|
31
|
+
def rank_for(search_statement)
|
32
|
+
ranks = search_statement.columns.map do |search_column|
|
33
|
+
Arel::Nodes::NamedFunction.new('similarity', [sanitize_column(search_column, search_statement.sanitizers), sanitize_string(search_statement.pattern, search_statement.sanitizers)])
|
34
|
+
end
|
35
|
+
join_expressions(ranks, :+)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Mincer
|
2
|
+
module Processors
|
3
|
+
module PgSearch
|
4
|
+
class SearchStatement
|
5
|
+
attr_accessor :columns, :options, :pattern
|
6
|
+
alias_method :terms, :pattern
|
7
|
+
|
8
|
+
def initialize(columns, options = {})
|
9
|
+
@columns, @options = columns, ::ActiveSupport::HashWithIndifferentAccess.new(options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def sanitizers
|
13
|
+
@sanitizers ||= Sanitizer::AVAILABLE_SANITIZERS.select { |sanitizer| options[sanitizer] }
|
14
|
+
end
|
15
|
+
|
16
|
+
def dictionary
|
17
|
+
options[:dictionary] || Mincer.config.pg_search.fulltext_engine[:dictionary]
|
18
|
+
end
|
19
|
+
|
20
|
+
def threshold
|
21
|
+
options[:threshold] || Mincer.config.pg_search.trigram_engine[:threshold]
|
22
|
+
end
|
23
|
+
|
24
|
+
def param_name
|
25
|
+
options[:param_name] || Mincer.config.pg_search.param_name
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Mincer
|
2
|
+
module Processors
|
3
|
+
module Sorting
|
4
|
+
class Processor
|
5
|
+
def initialize(mincer)
|
6
|
+
@mincer, @args, @relation = mincer, mincer.args, mincer.relation
|
7
|
+
end
|
8
|
+
|
9
|
+
def apply
|
10
|
+
relation = @relation.order(sort_string)
|
11
|
+
@mincer.sort_attribute, @mincer.sort_order = sort_attr, order_attr
|
12
|
+
relation
|
13
|
+
end
|
14
|
+
|
15
|
+
def sort_string
|
16
|
+
if sort_attr
|
17
|
+
"#{sort_attr} #{order_attr || default_order}"
|
18
|
+
else
|
19
|
+
"#{default_sort} #{order_attr || default_order}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def sort_attr
|
24
|
+
@mincer.send(:allowed_sort_attributes).include?(sort) && sort
|
25
|
+
end
|
26
|
+
|
27
|
+
def order_attr
|
28
|
+
%w{asc desc}.include?(order.try(:downcase)) && order
|
29
|
+
end
|
30
|
+
|
31
|
+
def sort
|
32
|
+
@args[::Mincer.config.sorting.sort_param_name]
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_sort
|
36
|
+
@mincer.try(:default_sort_attribute)
|
37
|
+
end
|
38
|
+
|
39
|
+
def order
|
40
|
+
@args[::Mincer.config.sorting.order_param_name]
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_order
|
44
|
+
@mincer.try(:default_sort_order)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
module Options
|
51
|
+
extend ActiveSupport::Concern
|
52
|
+
|
53
|
+
included do
|
54
|
+
# Used in view helpers
|
55
|
+
attr_accessor :sort_attribute, :sort_order
|
56
|
+
end
|
57
|
+
|
58
|
+
module ClassMethods
|
59
|
+
def skip_sorting!
|
60
|
+
active_processors.delete(Mincer::Processors::Sorting::Processor)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Default sort attribute. You must override this method if you want something else
|
65
|
+
def default_sort_attribute
|
66
|
+
::Mincer.config.sorting.sort_attribute
|
67
|
+
end
|
68
|
+
|
69
|
+
# Default order attribute. You must override this method if you want something else
|
70
|
+
def default_sort_order
|
71
|
+
::Mincer.config.sorting.order_attribute
|
72
|
+
end
|
73
|
+
|
74
|
+
# Allowed sort attributes, should return array of strings
|
75
|
+
def allowed_sort_attributes
|
76
|
+
@scope.attribute_names
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Configuration
|
81
|
+
include ActiveSupport::Configurable
|
82
|
+
|
83
|
+
config_accessor :sort_param_name do
|
84
|
+
:sort
|
85
|
+
end
|
86
|
+
|
87
|
+
config_accessor :sort_attribute do
|
88
|
+
:id
|
89
|
+
end
|
90
|
+
|
91
|
+
config_accessor :order_param_name do
|
92
|
+
:order
|
93
|
+
end
|
94
|
+
|
95
|
+
config_accessor :order_attribute do
|
96
|
+
:asc
|
97
|
+
end
|
98
|
+
|
99
|
+
config_accessor :asc_class do
|
100
|
+
'sorted order_down'
|
101
|
+
end
|
102
|
+
|
103
|
+
config_accessor :desc_class do
|
104
|
+
'sorted order_up'
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
::Mincer.add_processor(:sorting)
|
data/lib/mincer/version.rb
CHANGED
data/lib/mincer.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
+
require 'ostruct'
|
1
2
|
require 'mincer/version'
|
2
|
-
|
3
|
-
|
3
|
+
require 'mincer/core_ext/string'
|
4
|
+
require 'mincer/config'
|
4
5
|
require 'mincer/base'
|
5
6
|
|
6
7
|
module Mincer
|
@@ -15,24 +16,39 @@ module Mincer
|
|
15
16
|
def self.connection
|
16
17
|
::ActiveRecord::Base.connection()
|
17
18
|
end
|
18
|
-
end
|
19
19
|
|
20
|
+
def self.add_processor(processor)
|
21
|
+
processor_scope = ::Mincer::Processors.const_get(ActiveSupport::Inflector.camelize(processor.to_s, true))
|
22
|
+
::Mincer.processors << processor_scope.const_get('Processor')
|
23
|
+
::Mincer::Base.send(:include, processor_scope.const_get('Options')) if processor_scope.const_defined?('Options')
|
24
|
+
::Mincer.config.add(processor, processor_scope.const_get('Configuration')) if processor_scope.const_defined?('Configuration') if processor_scope.const_defined?('Configuration')
|
25
|
+
end
|
20
26
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
::Mincer::Processors.constants.each do |k|
|
28
|
-
klass = ::Mincer::Processors.const_get(k)
|
29
|
-
if klass.is_a?(Class)
|
30
|
-
::Mincer.processors << klass
|
31
|
-
elsif klass.is_a?(Module)
|
32
|
-
::Mincer::Base.send(:include, klass)
|
27
|
+
def self.pg_extension_installed?(extension)
|
28
|
+
@installed_extensions ||= {}
|
29
|
+
if @installed_extensions[extension.to_sym].nil?
|
30
|
+
@installed_extensions[extension.to_sym] = ::Mincer.connection.execute("SELECT DISTINCT p.proname FROM pg_proc p WHERE p.proname = '#{extension}'").count > 0
|
31
|
+
end
|
32
|
+
@installed_extensions[extension.to_sym]
|
33
33
|
end
|
34
|
+
|
34
35
|
end
|
35
36
|
|
37
|
+
# Loading helpers
|
38
|
+
require 'mincer/processors/helpers'
|
39
|
+
|
40
|
+
# Loading processors
|
41
|
+
require 'mincer/processors/sorting/processor'
|
42
|
+
require 'mincer/processors/pagination/processor'
|
43
|
+
require 'mincer/processors/pg_search/search_statement'
|
44
|
+
require 'mincer/processors/pg_search/sanitizer'
|
45
|
+
require 'mincer/processors/pg_search/search_engines/base'
|
46
|
+
require 'mincer/processors/pg_search/search_engines/array'
|
47
|
+
require 'mincer/processors/pg_search/search_engines/fulltext'
|
48
|
+
require 'mincer/processors/pg_search/search_engines/trigram'
|
49
|
+
require 'mincer/processors/pg_search/processor'
|
50
|
+
require 'mincer/processors/cache_digest/processor'
|
51
|
+
require 'mincer/processors/pg_json_dumper/processor'
|
36
52
|
|
37
53
|
# Loading ActionView helpers
|
38
54
|
if defined?(ActionView)
|
@@ -42,19 +58,3 @@ if defined?(ActionView)
|
|
42
58
|
ActionView::Base.send(:include, klass) if klass.is_a?(Module)
|
43
59
|
end
|
44
60
|
end
|
45
|
-
|
46
|
-
|
47
|
-
#if defined?(::Rails)
|
48
|
-
# module Mincer
|
49
|
-
# class Railtie < ::Rails::Railtie
|
50
|
-
# initializer 'mincer.setup_paths' do
|
51
|
-
# end
|
52
|
-
#
|
53
|
-
# initializer 'carrierwave.active_record' do
|
54
|
-
# #ActiveSupport.on_load :active_record do
|
55
|
-
# # require 'carrierwave/orm/activerecord'
|
56
|
-
# #end
|
57
|
-
# end
|
58
|
-
# end
|
59
|
-
# end
|
60
|
-
#end
|
data/mincer.gemspec
CHANGED
@@ -22,10 +22,8 @@ Gem::Specification.new do |spec|
|
|
22
22
|
|
23
23
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
24
24
|
spec.add_development_dependency 'rake'
|
25
|
-
spec.add_development_dependency 'rspec', '~> 2.14.1'
|
26
25
|
spec.add_development_dependency 'pg'
|
27
26
|
spec.add_development_dependency 'sqlite3'
|
28
27
|
spec.add_development_dependency 'kaminari'
|
29
28
|
spec.add_development_dependency 'will_paginate'
|
30
|
-
spec.add_development_dependency 'textacular'
|
31
29
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
describe ::Mincer::Base do
|
5
|
+
|
6
|
+
it 'passes all missing methods to relation' do
|
7
|
+
setup_basic_sqlite3_table
|
8
|
+
subject = Class.new(Mincer::Base) do
|
9
|
+
pg_search [{ columns: %w{"active_record_models"."tags" }, engines: [:array] }]
|
10
|
+
end
|
11
|
+
query = subject.new(ActiveRecordModel)
|
12
|
+
expect { query.attribute_names }.not_to raise_exception
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|