ar_finder_form 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.
@@ -0,0 +1,38 @@
1
+ require 'ar_finder_form'
2
+ module ArFinderForm
3
+ module ClientInstanceMethods
4
+ def find_options(value = nil)
5
+ @find_options = value if value
6
+ @find_options ||= self.class.find_options.dup
7
+ @find_options
8
+ end
9
+
10
+ def paginate_options(value = nil)
11
+ @find_options = value if value
12
+ @find_options ||= self.class.paginate_options.dup
13
+ @find_options
14
+ end
15
+
16
+ def to_find_options(options = {})
17
+ context = Context.build(self, options)
18
+ context.to_find_options
19
+ end
20
+
21
+ def to_paginate_options(options = {})
22
+ context = Context.build(self, options)
23
+ context.to_paginate_options
24
+ end
25
+
26
+ def find(*args)
27
+ options = to_find_options(args.extract_options!)
28
+ args << options
29
+ self.class.builder.model_class.find(*args)
30
+ end
31
+
32
+ def paginate(*args)
33
+ options = to_paginate_options(args.extract_options!)
34
+ args << options
35
+ self.class.builder.model_class.paginate(*args)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,89 @@
1
+ require 'ar_finder_form'
2
+ module ArFinderForm
3
+ class Column
4
+ attr_reader :table, :name, :options
5
+ attr_reader :form_attr
6
+ def initialize(table, name, *args)
7
+ @table, @name = table, name
8
+ @options = args.extract_options!
9
+ @static_values = args.empty? ? nil : args
10
+ end
11
+
12
+ def setup
13
+ send("setup_#{setup_type}")
14
+ end
15
+
16
+ def build(context)
17
+ @form_attr.build(context)
18
+ end
19
+
20
+ def static?
21
+ !!@static_values
22
+ end
23
+
24
+ def foreign_key?
25
+ name = self.name.to_s
26
+ table.model_class.reflections.
27
+ any?{|key, ref| ref.primary_key_name == name}
28
+ end
29
+
30
+ def model_column
31
+ result = table.model_column_for(name)
32
+ unless result.is_a?(ActiveRecord::ConnectionAdapters::Column)
33
+ raise "Unsupported column object for #{table.name}.#{name}: #{result.inspect}"
34
+ end
35
+ result
36
+ end
37
+
38
+ def type
39
+ @options[:type] || model_column.type
40
+ end
41
+
42
+ private
43
+ def setup_type
44
+ return 'match_static' if static?
45
+ return 'match_range' if options[:range]
46
+ return "match_#{options[:match]}" if options[:match]
47
+ case type
48
+ when :string, :text then
49
+ 'match_partial'
50
+ when :integer then
51
+ foreign_key? ? 'match_exactly' : 'match_range'
52
+ when :float, :datetime, :date, :time then
53
+ 'match_range'
54
+ else
55
+ 'match_exactly'
56
+ end
57
+ end
58
+
59
+ def setup_match_static
60
+ new_attr(Attr::Static, {:connector => 'AND'}, @static_values)
61
+ end
62
+
63
+ def setup_match_exactly
64
+ new_attr(Attr::Simple, :operator => '=')
65
+ end
66
+
67
+ def setup_match_range
68
+ new_attr(Attr::RangeAttrs, options.delete(:range))
69
+ end
70
+
71
+ Attr::Like::MATCHERS.keys.each do |matcher|
72
+ class_eval(<<-EOS)
73
+ def setup_match_#{matcher}
74
+ new_attr(Attr::Like, :match => :#{matcher.to_s})
75
+ end
76
+ EOS
77
+ end
78
+
79
+ def new_attr(klass, default_options, *args)
80
+ options = (default_options || {}).update(self.options)
81
+ args << options
82
+ @form_attr = klass.new(self, options[:attr] || name, *args)
83
+ @form_attr.setup
84
+ @form_attr
85
+ end
86
+
87
+
88
+ end
89
+ end
@@ -0,0 +1,6 @@
1
+ require 'ar_finder_form'
2
+ module ArFinderForm
3
+ class Config
4
+
5
+ end
6
+ end
@@ -0,0 +1,117 @@
1
+ require 'ar_finder_form'
2
+ module ArFinderForm
3
+ class Context
4
+ FIND_OPTIONS_KEYS = [:order, :group, :limit, :offset, :include,
5
+ :select, :from, :readonly, :lock
6
+ ]
7
+ PAGINATE_OPTIONS_KEYS = [:per_page, :page, :total_entries, :count, :finder]
8
+
9
+ attr_reader :form, :options, :joins
10
+ attr_reader :where, :params
11
+ attr_accessor :single_table
12
+
13
+ def initialize(form, options = {})
14
+ @form, @options = form, options
15
+ @where, @params = [], []
16
+ @joins = []
17
+ @connector = options.delete(:connector) || 'AND'
18
+ FIND_OPTIONS_KEYS.each do |attr_name|
19
+ if value = @options[attr_name]
20
+ @find_options ||= {}
21
+ @find_options[attr_name] = value
22
+ end
23
+ end
24
+ PAGINATE_OPTIONS_KEYS.each do |attr_name|
25
+ if value = @options[attr_name]
26
+ @paginate_options ||= {}
27
+ @paginate_options[attr_name] = value
28
+ end
29
+ end
30
+ end
31
+
32
+ def add_condition(where, *params)
33
+ @where << where
34
+ @params.concat(params)
35
+ end
36
+
37
+ def to_find_options(options = nil)
38
+ conditions = @where.join(" %s " % @connector)
39
+ unless @params.empty?
40
+ conditions = [conditions].concat(@params)
41
+ end
42
+ result = {}
43
+ if find_options
44
+ FIND_OPTIONS_KEYS.each do |attr_name|
45
+ value = find_options[attr_name]
46
+ result[attr_name] = value unless value.blank?
47
+ end
48
+ end
49
+ result[:joins] = joins.join(' ') unless joins.empty?
50
+ result[:conditions] = conditions unless conditions.empty?
51
+ options ? result.update(options) : result
52
+ end
53
+
54
+ def to_paginate_options(options = nil)
55
+ result = to_find_options(options)
56
+ if paginate_options
57
+ PAGINATE_OPTIONS_KEYS.each do |attr_name|
58
+ value = paginate_options[attr_name]
59
+ result[attr_name] = value unless value.blank?
60
+ end
61
+ end
62
+ result
63
+ end
64
+
65
+ def single_table?
66
+ @single_table
67
+ end
68
+
69
+ def new_sub_context(options = {})
70
+ result = Context.new(form, options)
71
+ result.single_table = self.single_table
72
+ result
73
+ end
74
+
75
+ def empty?
76
+ to_find_options[:conditions].nil? && joins.empty?
77
+ end
78
+
79
+ def merge(sub_context)
80
+ conditions = sub_context.to_find_options[:conditions]
81
+ if conditions
82
+ if conditions.is_a?(Array)
83
+ add_condition(conditions.shift, *conditions)
84
+ else
85
+ add_condition(conditions)
86
+ end
87
+ end
88
+ yield if block_given?
89
+ unless sub_context.joins.empty?
90
+ joins.concat(sub_context.joins)
91
+ end
92
+ end
93
+
94
+ def build(builder)
95
+ form.send(:before_build, self) if form.respond_to?(:before_build)
96
+ builder.build(self)
97
+ form.send(:after_build, self) if form.respond_to?(:after_build)
98
+ end
99
+
100
+ def find_options; @find_options ||= {}; end
101
+ def paginate_options; @paginate_options ||= {}; end
102
+
103
+ class << self
104
+ def build(form, options)
105
+ builder = form.class.builder
106
+ options =
107
+ (form.find_options || {}).dup.
108
+ update(form.find_options || {}).
109
+ update(options || {})
110
+ context = Context.new(form, options)
111
+ context.build(builder)
112
+ context
113
+ end
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,46 @@
1
+ require 'ar_finder_form'
2
+
3
+ module ArFinderForm
4
+ class JoinedTable < Table
5
+ attr_reader :reflection, :join_type, :parent_table
6
+ def initialize(parent_table, join_type, reflection, *args)
7
+ super(reflection.klass, *args)
8
+ @parent_table = parent_table
9
+ @join_type = join_type
10
+ @reflection = reflection
11
+ @table_name = reflection.klass.table_name
12
+ @name = "cond_" << @table_name
13
+ end
14
+
15
+ def table_name; @table_name; end
16
+ def name; @name; end
17
+
18
+ def root_table
19
+ parent_table.root_table
20
+ end
21
+
22
+ def build(context)
23
+ sub_context = context.new_sub_context(:connector => 'AND')
24
+ super(sub_context)
25
+ unless sub_context.empty?
26
+ context.merge(sub_context) do
27
+ context.joins << build_join
28
+ end
29
+ end
30
+ end
31
+
32
+ def build_join
33
+ join_on =
34
+ case reflection.macro
35
+ when :belongs_to then
36
+ "#{name}.id = #{parent_table.name}.#{reflection.primary_key_name}"
37
+ else
38
+ "#{name}.#{reflection.primary_key_name} = #{parent_table.name}.id"
39
+ end
40
+ "%s JOIN %s %s ON %s" % [
41
+ join_type.to_s.gsub(/\_/, ' ').upcase, reflection.klass.table_name, name, join_on
42
+ ]
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,84 @@
1
+ require 'ar_finder_form'
2
+
3
+ class NameAccessableArray < Array
4
+ attr_accessor :item_name
5
+ def initialize(item_name, *args, &block)
6
+ super(*args, &block)
7
+ @item_name = item_name
8
+ end
9
+
10
+ def [](index_or_name)
11
+ if index_or_name.is_a?(Integer)
12
+ super.[](index_or_name)
13
+ else
14
+ detect{|item| name_for(item) == index_or_name}
15
+ end
16
+ end
17
+
18
+ def name_for(item)
19
+ item.send(item_name)
20
+ end
21
+ end
22
+
23
+ module ArFinderForm
24
+ class Table
25
+ attr_reader :model_class, :columns, :joined_tables
26
+ def initialize(model_class, *args)
27
+ @model_class = model_class
28
+ @columns = NameAccessableArray.new(:name)
29
+ @joined_tables = []
30
+ end
31
+
32
+ def table_name; @model_class.table_name; end
33
+ def name; table_name; end
34
+
35
+ def column(column_name, *args)
36
+ @columns << Column.new(self, column_name, *args)
37
+ end
38
+
39
+ def model_column_for(name)
40
+ name = name.to_s
41
+ @model_columns ||= @model_class.columns
42
+ @model_columns.detect{|col| col.name.to_s == name}
43
+ end
44
+
45
+ def build_methods
46
+ columns.each{|column| column.setup}
47
+ joined_tables.each do |joined_table|
48
+ joined_table.build_methods
49
+ end
50
+ end
51
+
52
+ def build(context)
53
+ columns.each{|column| column.build(context)}
54
+ joined_tables.each do |joined_table|
55
+ joined_table.build(context)
56
+ end
57
+ end
58
+
59
+ def join(join_type, options, &block)
60
+ join_as = options.delete(:as)
61
+ join_on = options.delete(:on)
62
+ ref_name = [:belongs_to, :has_one, :has_many].map{|k| options[k]}.compact.first
63
+ raise ArgumentError, "#{join_type}_join requires :belongs_to, :has_one or :has_many" unless ref_name
64
+ ref = @model_class.reflections[ref_name]
65
+ raise ArgumentError, "no reflection for #{ref_name.inspect}" unless ref
66
+ result = JoinedTable.new(self, join_type, ref)
67
+ @joined_tables << result
68
+ result.instance_eval(&block)
69
+ result
70
+ end
71
+
72
+ JOIN_TYPES = (%w(inner cross natual) +
73
+ %w(left right full).map{|t| [t, "#{t}_outer"]}.flatten).map(&:to_sym)
74
+
75
+ JOIN_TYPES.each do |join_type|
76
+ class_eval(<<-"EOS")
77
+ def #{join_type.to_s}_join(options, &block)
78
+ join(:#{join_type.to_s}, options, &block)
79
+ end
80
+ EOS
81
+ end
82
+
83
+ end
84
+ end
data/spec/.gitignore ADDED
@@ -0,0 +1 @@
1
+ /*.sqlite3
data/spec/database.yml ADDED
@@ -0,0 +1,23 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: finder_form_plugin_test.sqlite3
4
+ timeout: 5000
5
+
6
+ mysql:
7
+ adapter: mysql
8
+ database: finder_form_test
9
+ encoding: utf8
10
+ username: root
11
+ password:
12
+ socket: /opt/local/var/run/mysql5/mysqld.sock
13
+ # connect_timeout: 5000
14
+ read_timeout: 3000
15
+ write_timeout: 5000
16
+
17
+ postgresql:
18
+ adapter: postgresql
19
+ database: finder_form_test
20
+ encoding: unicode
21
+ host: localhost
22
+ username: postgres
23
+ password:
@@ -0,0 +1,250 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.join(File.dirname(__FILE__), 'spec_helper')
3
+
4
+ class OrderFinderForm1
5
+ include ArFinderForm
6
+
7
+ def initialize(attrs = {})
8
+ attrs.each{|key, value|send("#{key}=", value)}
9
+ end
10
+
11
+ with_model(Order) do
12
+ # 静的なパラメータ
13
+ column(:deleted_at, "IS NOT NULL")
14
+
15
+ # belongs_to単数一致
16
+ column(:user_id, :attr => :user_id)
17
+ # belongs_toに使われているintegerはデフォルトでは一致と判断されます。
18
+
19
+ # belongs_to複数一致
20
+ column(:product_id, :attr => :product_ids, :operator => :IN)
21
+ # :operatorが指定されているので、単数の一致ではなく複数のどれかへの一致になります
22
+
23
+ # 範囲integer
24
+ column(:amount)
25
+ # belongs_toに使われていないintegerはデフォルトでは範囲と判断されます。
26
+ # = column(:amount, :range => {:min => {:attr => :amount_min, :oprator => '>='}, :max => {:attr => :amount_max, :oprator => '<='}})
27
+
28
+ # 範囲float
29
+ column(:price)
30
+ # dateはデフォルトでは範囲と判断されます。
31
+ # => column_range(:price)
32
+ # column_rangeにはデフォルトで :minに'>='と:maxに'<='が指定されます。
33
+ # = column(:price, :range => {:min => {:attr => :price_min, :oprator => '>='}, :max => {:attr => :price_max, :oprator => '<='}})
34
+
35
+ # 範囲date
36
+ column(:delivery_estimate)
37
+ # dateはデフォルトでは範囲と判断されます。
38
+ # => column_range(:delivery_estimate)
39
+ # column_rangeにはデフォルトで :minに'>='と:maxに'<='が指定されます。
40
+ # => column_range(:delivery_estimate, :min => '>=', :max => '<=')
41
+
42
+ # 範囲time
43
+ column(:delivered_at)
44
+ # timeはデフォルトでは範囲と判断されます。
45
+ # => column_range(:delivered_at)
46
+ # column_rangeにはデフォルトで :minに'>='と:maxに'<='が指定されます。
47
+ # => column_range(:delivered_at, :min => '>=', :max => '<=')
48
+
49
+ end
50
+ end
51
+
52
+
53
+ describe OrderFinderForm1 do
54
+
55
+ after do
56
+ Order.find(:all, @form.to_find_options)
57
+ end
58
+
59
+ it "no attribute" do
60
+ @form = OrderFinderForm1.new
61
+ @form.to_find_options.should == {
62
+ :conditions => "deleted_at IS NOT NULL"
63
+ }
64
+ end
65
+
66
+ describe "belongs_to" do
67
+ it "with integer" do
68
+ @form = OrderFinderForm1.new(:user_id => 3)
69
+ @form.to_find_options.should == {
70
+ :conditions => ["deleted_at IS NOT NULL AND user_id = ?", 3]}
71
+ end
72
+
73
+ it "with string" do
74
+ @form = OrderFinderForm1.new(:user_id => '3')
75
+ @form.to_find_options.should == {
76
+ :conditions => ["deleted_at IS NOT NULL AND user_id = ?", 3]}
77
+ end
78
+ end
79
+
80
+
81
+ describe "belongs_to IN" do
82
+ it "with integer array" do
83
+ @form = OrderFinderForm1.new(:product_ids => [1,2,3,4])
84
+ @form.to_find_options.should == {
85
+ :conditions => ["deleted_at IS NOT NULL AND product_id IN (?)", [1,2,3,4]]}
86
+ end
87
+
88
+ it "with String array" do
89
+ @form = OrderFinderForm1.new(:product_ids => %w(3 4 6 8))
90
+ @form.to_find_options.should == {
91
+ :conditions => ["deleted_at IS NOT NULL AND product_id IN (?)", [3, 4, 6, 8]]}
92
+ end
93
+
94
+ it "with comma separated string" do
95
+ @form = OrderFinderForm1.new(:product_ids => '1,2,3,4')
96
+ @form.to_find_options.should == {
97
+ :conditions => ["deleted_at IS NOT NULL AND product_id IN (?)", [1,2,3,4]]}
98
+ end
99
+
100
+ end
101
+
102
+ describe "integer range" do
103
+ describe "as integer" do
104
+ it "with min" do
105
+ @form = OrderFinderForm1.new(:amount_min => 3)
106
+ @form.to_find_options.should == {
107
+ :conditions => ["deleted_at IS NOT NULL AND amount >= ?", 3]}
108
+ end
109
+
110
+ it "with max" do
111
+ @form = OrderFinderForm1.new(:amount_max => 10)
112
+ @form.to_find_options.should == {
113
+ :conditions => ["deleted_at IS NOT NULL AND amount <= ?", 10]}
114
+ end
115
+
116
+ it "with min and max" do
117
+ @form = OrderFinderForm1.new(:amount_min => 4, :amount_max => 9)
118
+ @form.to_find_options.should == {
119
+ :conditions => ["deleted_at IS NOT NULL AND amount >= ? AND amount <= ?", 4, 9]}
120
+ end
121
+ end
122
+
123
+ describe "as string" do
124
+ it "with min" do
125
+ @form = OrderFinderForm1.new(:amount_min => '3')
126
+ @form.to_find_options.should == {
127
+ :conditions => ["deleted_at IS NOT NULL AND amount >= ?", 3]}
128
+ end
129
+
130
+ it "with max" do
131
+ @form = OrderFinderForm1.new(:amount_max => '10')
132
+ @form.to_find_options.should == {
133
+ :conditions => ["deleted_at IS NOT NULL AND amount <= ?", 10]}
134
+ end
135
+
136
+ it "with min and max" do
137
+ @form = OrderFinderForm1.new(:amount_min => '4', :amount_max => '9')
138
+ @form.to_find_options.should == {
139
+ :conditions => ["deleted_at IS NOT NULL AND amount >= ? AND amount <= ?", 4, 9]}
140
+ end
141
+ end
142
+ end
143
+
144
+ describe "float range" do
145
+ describe "as float" do
146
+ it "with min" do
147
+ @form = OrderFinderForm1.new(:price_min => 3.9)
148
+ @form.to_find_options.should == {
149
+ :conditions => ["deleted_at IS NOT NULL AND price >= ?", 3.9]}
150
+ end
151
+
152
+ it "with max" do
153
+ @form = OrderFinderForm1.new(:price_max => 10.2)
154
+ @form.to_find_options.should == {
155
+ :conditions => ["deleted_at IS NOT NULL AND price <= ?", 10.2]}
156
+ end
157
+
158
+ it "with comma separated string" do
159
+ @form = OrderFinderForm1.new(:price_min => 4.1, :price_max => 9.9)
160
+ @form.to_find_options.should == {
161
+ :conditions => ["deleted_at IS NOT NULL AND price >= ? AND price <= ?", 4.1, 9.9]}
162
+ end
163
+ end
164
+
165
+ describe "as String" do
166
+ it "with min" do
167
+ @form = OrderFinderForm1.new(:price_min => '3.1')
168
+ @form.to_find_options.should == {
169
+ :conditions => ["deleted_at IS NOT NULL AND price >= ?", 3.1]}
170
+ end
171
+
172
+ it "with max" do
173
+ @form = OrderFinderForm1.new(:price_max => '10.1')
174
+ @form.to_find_options.should == {
175
+ :conditions => ["deleted_at IS NOT NULL AND price <= ?", 10.1]}
176
+ end
177
+
178
+ it "with comma separated string" do
179
+ @form = OrderFinderForm1.new(:price_min => 4.0, :price_max => 9.5)
180
+ @form.to_find_options.should == {
181
+ :conditions => ["deleted_at IS NOT NULL AND price >= ? AND price <= ?", 4.0, 9.5]}
182
+ end
183
+ end
184
+ end
185
+
186
+
187
+
188
+ describe "date range" do
189
+ describe "as Date" do
190
+ it "with min" do
191
+ @form = OrderFinderForm1.new(:delivery_estimate_min => Date.parse("2009/11/15"))
192
+ @form.to_find_options.should == {
193
+ :conditions => ["deleted_at IS NOT NULL AND delivery_estimate >= ?", Date.parse("2009/11/15")]}
194
+ end
195
+
196
+ it "with max" do
197
+ @form = OrderFinderForm1.new(:delivery_estimate_max => Date.parse("2009/11/15"))
198
+ @form.to_find_options.should == {
199
+ :conditions => ["deleted_at IS NOT NULL AND delivery_estimate <= ?", Date.parse("2009/11/15")]}
200
+ end
201
+
202
+ it "with comma separated string" do
203
+ @form = OrderFinderForm1.new(:delivery_estimate_min => Date.parse("2009/11/1"), :delivery_estimate_max => Date.parse("2009/11/15"))
204
+ @form.to_find_options.should == {
205
+ :conditions => ["deleted_at IS NOT NULL AND delivery_estimate >= ? AND delivery_estimate <= ?", Date.parse("2009/11/1"), Date.parse("2009/11/15")]}
206
+ end
207
+ end
208
+
209
+ describe "as DateTime" do
210
+ it "with min" do
211
+ @form = OrderFinderForm1.new(:delivery_estimate_min => DateTime.parse("2009/11/15 12:34:56"))
212
+ @form.to_find_options.should == {
213
+ :conditions => ["deleted_at IS NOT NULL AND delivery_estimate >= ?", DateTime.parse("2009/11/15 12:34:56")]}
214
+ end
215
+
216
+ it "with max" do
217
+ @form = OrderFinderForm1.new(:delivery_estimate_max => DateTime.parse("2009/11/15 01:02:03"))
218
+ @form.to_find_options.should == {
219
+ :conditions => ["deleted_at IS NOT NULL AND delivery_estimate <= ?", DateTime.parse("2009/11/15 01:02:03")]}
220
+ end
221
+
222
+ it "with comma separated string" do
223
+ @form = OrderFinderForm1.new(:delivery_estimate_min => DateTime.parse("2009/11/1 01:02:03"), :delivery_estimate_max => DateTime.parse("2009/11/15 11:32:33"))
224
+ @form.to_find_options.should == {
225
+ :conditions => ["deleted_at IS NOT NULL AND delivery_estimate >= ? AND delivery_estimate <= ?", DateTime.parse("2009/11/1 01:02:03"), DateTime.parse("2009/11/15 11:32:33")]}
226
+ end
227
+ end
228
+
229
+ describe "as String" do
230
+ it "with min" do
231
+ @form = OrderFinderForm1.new(:delivery_estimate_min => "2009-11-1")
232
+ @form.to_find_options.should == {
233
+ :conditions => ["deleted_at IS NOT NULL AND delivery_estimate >= ?", Date.parse("2009/11/1")]}
234
+ end
235
+
236
+ it "with max" do
237
+ @form = OrderFinderForm1.new(:delivery_estimate_max => "H21.11.15")
238
+ @form.to_find_options.should == {
239
+ :conditions => ["deleted_at IS NOT NULL AND delivery_estimate <= ?", Date.parse("2009/11/15")]}
240
+ end
241
+
242
+ it "with comma separated string" do
243
+ @form = OrderFinderForm1.new(:delivery_estimate_min => "2009/11/1", :delivery_estimate_max => "2009/11/15")
244
+ @form.to_find_options.should == {
245
+ :conditions => ["deleted_at IS NOT NULL AND delivery_estimate >= ? AND delivery_estimate <= ?", Date.parse("2009/11/1"), Date.parse("2009/11/15")]}
246
+ end
247
+ end
248
+ end
249
+
250
+ end