ransack 0.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.
Files changed (51) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +11 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +5 -0
  5. data/Rakefile +19 -0
  6. data/lib/ransack.rb +24 -0
  7. data/lib/ransack/adapters/active_record.rb +2 -0
  8. data/lib/ransack/adapters/active_record/base.rb +17 -0
  9. data/lib/ransack/adapters/active_record/context.rb +153 -0
  10. data/lib/ransack/configuration.rb +39 -0
  11. data/lib/ransack/constants.rb +23 -0
  12. data/lib/ransack/context.rb +152 -0
  13. data/lib/ransack/helpers.rb +2 -0
  14. data/lib/ransack/helpers/form_builder.rb +172 -0
  15. data/lib/ransack/helpers/form_helper.rb +27 -0
  16. data/lib/ransack/locale/en.yml +67 -0
  17. data/lib/ransack/naming.rb +53 -0
  18. data/lib/ransack/nodes.rb +7 -0
  19. data/lib/ransack/nodes/and.rb +8 -0
  20. data/lib/ransack/nodes/attribute.rb +36 -0
  21. data/lib/ransack/nodes/condition.rb +209 -0
  22. data/lib/ransack/nodes/grouping.rb +207 -0
  23. data/lib/ransack/nodes/node.rb +34 -0
  24. data/lib/ransack/nodes/or.rb +8 -0
  25. data/lib/ransack/nodes/sort.rb +39 -0
  26. data/lib/ransack/nodes/value.rb +120 -0
  27. data/lib/ransack/predicate.rb +57 -0
  28. data/lib/ransack/search.rb +114 -0
  29. data/lib/ransack/translate.rb +92 -0
  30. data/lib/ransack/version.rb +3 -0
  31. data/ransack.gemspec +29 -0
  32. data/spec/blueprints/articles.rb +5 -0
  33. data/spec/blueprints/comments.rb +5 -0
  34. data/spec/blueprints/notes.rb +3 -0
  35. data/spec/blueprints/people.rb +4 -0
  36. data/spec/blueprints/tags.rb +3 -0
  37. data/spec/console.rb +22 -0
  38. data/spec/helpers/ransack_helper.rb +2 -0
  39. data/spec/playground.rb +37 -0
  40. data/spec/ransack/adapters/active_record/base_spec.rb +30 -0
  41. data/spec/ransack/adapters/active_record/context_spec.rb +29 -0
  42. data/spec/ransack/configuration_spec.rb +11 -0
  43. data/spec/ransack/helpers/form_builder_spec.rb +39 -0
  44. data/spec/ransack/nodes/compound_condition_spec.rb +0 -0
  45. data/spec/ransack/nodes/condition_spec.rb +0 -0
  46. data/spec/ransack/nodes/grouping_spec.rb +13 -0
  47. data/spec/ransack/predicate_spec.rb +25 -0
  48. data/spec/ransack/search_spec.rb +182 -0
  49. data/spec/spec_helper.rb +28 -0
  50. data/spec/support/schema.rb +102 -0
  51. metadata +200 -0
@@ -0,0 +1,207 @@
1
+ module Ransack
2
+ module Nodes
3
+ class Grouping < Node
4
+ attr_reader :conditions
5
+ i18n_word :condition, :and, :or
6
+ i18n_alias :c => :condition, :n => :and, :o => :or
7
+
8
+ delegate :each, :to => :values
9
+
10
+ def persisted?
11
+ false
12
+ end
13
+
14
+ def translate(key, options = {})
15
+ super or Translate.attribute(key.to_s, options.merge(:context => context))
16
+ end
17
+
18
+ def conditions
19
+ @conditions ||= []
20
+ end
21
+ alias :c :conditions
22
+
23
+ def conditions=(conditions)
24
+ case conditions
25
+ when Array
26
+ conditions.each do |attrs|
27
+ condition = Condition.new(@context).build(attrs)
28
+ self.conditions << condition if condition.valid?
29
+ end
30
+ when Hash
31
+ conditions.each do |index, attrs|
32
+ condition = Condition.new(@context).build(attrs)
33
+ self.conditions << condition if condition.valid?
34
+ end
35
+ end
36
+
37
+ self.conditions.uniq!
38
+ end
39
+ alias :c= :conditions=
40
+
41
+ def [](key)
42
+ if condition = conditions.detect {|c| c.key == key.to_s}
43
+ condition
44
+ else
45
+ nil
46
+ end
47
+ end
48
+
49
+ def []=(key, value)
50
+ conditions.reject! {|c| c.key == key.to_s}
51
+ self.conditions << value
52
+ end
53
+
54
+ def values
55
+ conditions + ors + ands
56
+ end
57
+
58
+ def respond_to?(method_id)
59
+ super or begin
60
+ method_name = method_id.to_s
61
+ writer = method_name.sub!(/\=$/, '')
62
+ attribute_method?(method_name) ? true : false
63
+ end
64
+ end
65
+
66
+ def build_condition(opts = {})
67
+ new_condition(opts).tap do |condition|
68
+ self.conditions << condition
69
+ end
70
+ end
71
+
72
+ def new_condition(opts = {})
73
+ attrs = opts[:attributes] || 1
74
+ vals = opts[:values] || 1
75
+ condition = Condition.new(@context)
76
+ condition.predicate = Predicate.named('eq')
77
+ attrs.times { condition.build_attribute }
78
+ vals.times { condition.build_value }
79
+ condition
80
+ end
81
+
82
+ def ands
83
+ @ands ||= []
84
+ end
85
+ alias :n :ands
86
+
87
+ def ands=(ands)
88
+ case ands
89
+ when Array
90
+ ands.each do |attrs|
91
+ and_object = And.new(@context).build(attrs)
92
+ self.ands << and_object if and_object.values.any?
93
+ end
94
+ when Hash
95
+ ands.each do |index, attrs|
96
+ and_object = And.new(@context).build(attrs)
97
+ self.ands << and_object if and_object.values.any?
98
+ end
99
+ else
100
+ raise ArgumentError, "Invalid argument (#{ands.class}) supplied to ands="
101
+ end
102
+ end
103
+ alias :n= :ands=
104
+
105
+ def ors
106
+ @ors ||= []
107
+ end
108
+ alias :o :ors
109
+
110
+ def ors=(ors)
111
+ case ors
112
+ when Array
113
+ ors.each do |attrs|
114
+ or_object = Or.new(@context).build(attrs)
115
+ self.ors << or_object if or_object.values.any?
116
+ end
117
+ when Hash
118
+ ors.each do |index, attrs|
119
+ or_object = Or.new(@context).build(attrs)
120
+ self.ors << or_object if or_object.values.any?
121
+ end
122
+ else
123
+ raise ArgumentError, "Invalid argument (#{ors.class}) supplied to ors="
124
+ end
125
+ end
126
+ alias :o= :ors=
127
+
128
+ def method_missing(method_id, *args)
129
+ method_name = method_id.to_s
130
+ writer = method_name.sub!(/\=$/, '')
131
+ if attribute_method?(method_name)
132
+ writer ? write_attribute(method_name, *args) : read_attribute(method_name)
133
+ else
134
+ super
135
+ end
136
+ end
137
+
138
+ def attribute_method?(name)
139
+ name = strip_predicate_and_index(name)
140
+ case name
141
+ when /^(n|o|c|ands|ors|conditions)=?$/
142
+ true
143
+ else
144
+ name.split(/_and_|_or_/).select {|n| !@context.attribute_method?(n)}.empty?
145
+ end
146
+ end
147
+
148
+ def build_and(params = {})
149
+ params ||= {}
150
+ new_and(params).tap do |new_and|
151
+ self.ands << new_and
152
+ end
153
+ end
154
+
155
+ def new_and(params = {})
156
+ And.new(@context).build(params)
157
+ end
158
+
159
+ def build_or(params = {})
160
+ params ||= {}
161
+ new_or(params).tap do |new_or|
162
+ self.ors << new_or
163
+ end
164
+ end
165
+
166
+ def new_or(params = {})
167
+ Or.new(@context).build(params)
168
+ end
169
+
170
+ def build(params)
171
+ params.with_indifferent_access.each do |key, value|
172
+ case key
173
+ when /^(n|o|c)$/
174
+ self.send("#{key}=", value)
175
+ else
176
+ write_attribute(key.to_s, value)
177
+ end
178
+ end
179
+ self
180
+ end
181
+
182
+ private
183
+
184
+ def write_attribute(name, val)
185
+ # TODO: Methods
186
+ if condition = Condition.extract(@context, name, val)
187
+ self[name] = condition
188
+ end
189
+ end
190
+
191
+ def read_attribute(name)
192
+ if self[name].respond_to?(:value)
193
+ self[name].value
194
+ else
195
+ self[name]
196
+ end
197
+ end
198
+
199
+ def strip_predicate_and_index(str)
200
+ string = str.split(/\(/).first
201
+ Ransack::Configuration.predicate_keys.detect {|p| string.sub!(/_#{p}$/, '')}
202
+ string
203
+ end
204
+
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,34 @@
1
+ module Ransack
2
+ module Nodes
3
+ class Node
4
+ attr_reader :context
5
+ delegate :contextualize, :to => :context
6
+ class_attribute :i18n_words
7
+ class_attribute :i18n_aliases
8
+ self.i18n_words = []
9
+ self.i18n_aliases = {}
10
+
11
+ class << self
12
+ def i18n_word(*args)
13
+ self.i18n_words += args.map(&:to_s)
14
+ end
15
+
16
+ def i18n_alias(opts = {})
17
+ self.i18n_aliases.merge! Hash[opts.map {|k, v| [k.to_s, v.to_s]}]
18
+ end
19
+ end
20
+
21
+ def initialize(context)
22
+ @context = context
23
+ end
24
+
25
+ def translate(key, options = {})
26
+ key = i18n_aliases[key.to_s] if i18n_aliases.has_key?(key.to_s)
27
+ if i18n_words.include?(key.to_s)
28
+ Translate.word(key)
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,8 @@
1
+ require 'ransack/nodes/grouping'
2
+
3
+ module Ransack
4
+ module Nodes
5
+ class Or < Grouping
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,39 @@
1
+ module Ransack
2
+ module Nodes
3
+ class Sort < Node
4
+ attr_reader :name, :attr, :dir
5
+ i18n_word :asc, :desc
6
+
7
+ class << self
8
+ def extract(context, str)
9
+ attr, direction = str.split(/\s+/,2)
10
+ self.new(context).build(:name => attr, :dir => direction)
11
+ end
12
+ end
13
+
14
+ def build(params)
15
+ params.with_indifferent_access.each do |key, value|
16
+ if key.match(/^(name|dir)$/)
17
+ self.send("#{key}=", value)
18
+ end
19
+ end
20
+
21
+ self
22
+ end
23
+
24
+ def valid?
25
+ @attr
26
+ end
27
+
28
+ def name=(name)
29
+ @name = name
30
+ @attr = contextualize(name) unless name.blank?
31
+ end
32
+
33
+ def dir=(dir)
34
+ @dir = %w(asc desc).include?(dir) ? dir : 'asc'
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,120 @@
1
+ module Ransack
2
+ module Nodes
3
+ class Value < Node
4
+ attr_reader :value_before_cast, :type
5
+ delegate :blank?, :to => :value_before_cast
6
+
7
+ def initialize(context, value = nil, type = nil)
8
+ super(context)
9
+ @value_before_cast = value
10
+ self.type = type if type
11
+ end
12
+
13
+ def value=(val)
14
+ @value_before_cast = value
15
+ @value = nil
16
+ end
17
+
18
+ def value
19
+ @value ||= cast_to_type(@value_before_cast, @type)
20
+ end
21
+
22
+ def persisted?
23
+ false
24
+ end
25
+
26
+ def eql?(other)
27
+ self.class == other.class &&
28
+ self.value_before_cast == other.value_before_cast
29
+ end
30
+ alias :== :eql?
31
+
32
+ def hash
33
+ value_before_cast.hash
34
+ end
35
+
36
+ def type=(type)
37
+ @value = nil
38
+ @type = type
39
+ end
40
+
41
+ def cast_to_type(val, type)
42
+ case type
43
+ when :date
44
+ cast_to_date(val)
45
+ when :datetime, :timestamp, :time
46
+ cast_to_time(val)
47
+ when :boolean
48
+ cast_to_boolean(val)
49
+ when :integer
50
+ cast_to_integer(val)
51
+ when :float
52
+ cast_to_float(val)
53
+ when :decimal
54
+ cast_to_decimal(val)
55
+ else
56
+ cast_to_string(val)
57
+ end
58
+ end
59
+
60
+ def cast_to_date(val)
61
+ if val.respond_to?(:to_date)
62
+ val.to_date rescue nil
63
+ else
64
+ y, m, d = *[val].flatten
65
+ m ||= 1
66
+ d ||= 1
67
+ Date.new(y,m,d) rescue nil
68
+ end
69
+ end
70
+
71
+ # FIXME: doesn't seem to be casting, even with Time.zone.local
72
+ def cast_to_time(val)
73
+ if val.is_a?(Array)
74
+ Time.zone.local(*val) rescue nil
75
+ else
76
+ unless val.acts_like?(:time)
77
+ val = val.is_a?(String) ? Time.zone.parse(val) : val.to_time rescue val
78
+ end
79
+ val.in_time_zone
80
+ end
81
+ end
82
+
83
+ def cast_to_boolean(val)
84
+ if val.is_a?(String) && val.blank?
85
+ nil
86
+ else
87
+ Constants::TRUE_VALUES.include?(val)
88
+ end
89
+ end
90
+
91
+ def cast_to_string(val)
92
+ val.respond_to?(:to_s) ? val.to_s : String.new(val)
93
+ end
94
+
95
+ def cast_to_integer(val)
96
+ val.blank? ? nil : val.to_i
97
+ end
98
+
99
+ def cast_to_float(val)
100
+ val.blank? ? nil : val.to_f
101
+ end
102
+
103
+ def cast_to_decimal(val)
104
+ if val.blank?
105
+ nil
106
+ elsif val.class == BigDecimal
107
+ val
108
+ elsif val.respond_to?(:to_d)
109
+ val.to_d
110
+ else
111
+ val.to_s.to_d
112
+ end
113
+ end
114
+
115
+ def array_of_arrays?(val)
116
+ Array === val && Array === val.first
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,57 @@
1
+ module Ransack
2
+ class Predicate
3
+ attr_reader :name, :arel_predicate, :type, :formatter, :validator, :compound
4
+
5
+ class << self
6
+ def named(name)
7
+ Configuration.predicates[name.to_s]
8
+ end
9
+
10
+ def for_attribute_name(attribute_name)
11
+ self.named(Configuration.predicate_keys.detect {|p| attribute_name.to_s.match(/_#{p}$/)})
12
+ end
13
+
14
+ def collection
15
+ Configuration.predicates.map {|k, v| [k, Translate.predicate(k)]}
16
+ end
17
+ end
18
+
19
+ def initialize(opts = {})
20
+ @name = opts[:name]
21
+ @arel_predicate = opts[:arel_predicate]
22
+ @type = opts[:type]
23
+ @formatter = opts[:formatter]
24
+ @validator = opts[:validator]
25
+ @compound = opts[:compound]
26
+ end
27
+
28
+ def format(vals)
29
+ if formatter
30
+ vals.select {|v| validator ? validator.call(v.value_before_cast) : !v.blank?}.
31
+ map {|v| formatter.call(v.value)}
32
+ else
33
+ vals.select {|v| validator ? validator.call(v.value_before_cast) : !v.blank?}.
34
+ map {|v| v.value}
35
+ end
36
+ end
37
+
38
+ def eql?(other)
39
+ self.class == other.class &&
40
+ self.name == other.name
41
+ end
42
+ alias :== :eql?
43
+
44
+ def hash
45
+ name.hash
46
+ end
47
+
48
+ def validate(vals)
49
+ if validator
50
+ vals.select {|v| validator.call(v.value_before_cast)}.any?
51
+ else
52
+ vals.select {|v| !v.blank?}.any?
53
+ end
54
+ end
55
+
56
+ end
57
+ end