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.
- data/.gitignore +2 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +117 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/ar_finder_form.rb +25 -0
- data/lib/ar_finder_form/attr.rb +11 -0
- data/lib/ar_finder_form/attr/base.rb +49 -0
- data/lib/ar_finder_form/attr/like.rb +30 -0
- data/lib/ar_finder_form/attr/range_attrs.rb +29 -0
- data/lib/ar_finder_form/attr/simple.rb +29 -0
- data/lib/ar_finder_form/attr/static.rb +24 -0
- data/lib/ar_finder_form/builder.rb +22 -0
- data/lib/ar_finder_form/client_class_methods.rb +23 -0
- data/lib/ar_finder_form/client_instance_methods.rb +38 -0
- data/lib/ar_finder_form/column.rb +89 -0
- data/lib/ar_finder_form/config.rb +6 -0
- data/lib/ar_finder_form/context.rb +117 -0
- data/lib/ar_finder_form/joined_table.rb +46 -0
- data/lib/ar_finder_form/table.rb +84 -0
- data/spec/.gitignore +1 -0
- data/spec/database.yml +23 -0
- data/spec/order_finder_form1_spec.rb +250 -0
- data/spec/order_finder_form2_spec.rb +82 -0
- data/spec/resources/models/order.rb +5 -0
- data/spec/resources/models/product.rb +11 -0
- data/spec/resources/models/user.rb +4 -0
- data/spec/schema.rb +30 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +83 -0
- data/spec/table_spec.rb +48 -0
- data/spec/user_finder_form1_spec.rb +94 -0
- data/spec/with_selectable_attr_rails_spec.rb +66 -0
- data/tasks/finder_form_tasks.rake +4 -0
- data/uninstall.rb +1 -0
- metadata +109 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Takeshi AKIMA
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
= ArFinderForm
|
2
|
+
== ArFinderFormとは?
|
3
|
+
|
4
|
+
検索条件フォームを少ない記述で組み立てるためのDSLを提供するライブラリです。
|
5
|
+
|
6
|
+
== サンプル
|
7
|
+
|
8
|
+
以下のようなデータベースがあった場合、
|
9
|
+
ActiveRecord::Schema.define(:version => 0) do
|
10
|
+
create_table :products, :force => true do |t|
|
11
|
+
t.string :category_cd
|
12
|
+
t.string :code
|
13
|
+
t.string :name
|
14
|
+
t.float :price
|
15
|
+
t.integer :stock
|
16
|
+
t.time :released_at
|
17
|
+
t.timestamp
|
18
|
+
end
|
19
|
+
|
20
|
+
create_table :orders, :force => true do |t|
|
21
|
+
t.integer :user_id
|
22
|
+
t.integer :product_id
|
23
|
+
t.integer :amount
|
24
|
+
t.float :price
|
25
|
+
t.date :delivery_estimate
|
26
|
+
t.time :delivered_at
|
27
|
+
t.time :deleted_at
|
28
|
+
t.timestamp
|
29
|
+
end
|
30
|
+
|
31
|
+
create_table :users, :force => true do |t|
|
32
|
+
t.string :login
|
33
|
+
t.string :name
|
34
|
+
t.timestamp
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
例えば、Userのfindに渡すオプションを組み立てるクラスを以下のように組み立てることができます。
|
40
|
+
|
41
|
+
class UserFinderForm1
|
42
|
+
include ArFinderForm
|
43
|
+
|
44
|
+
def initialize(attrs = {})
|
45
|
+
attrs.each{|key, value|send("#{key}=", value)}
|
46
|
+
end
|
47
|
+
|
48
|
+
with_model(User) do
|
49
|
+
# users.nameの条件。文字列なのでデフォルトでLIKEが使われます
|
50
|
+
column(:name, :attr => :user_name)
|
51
|
+
|
52
|
+
# 関連するordersに関する条件を加えるため、ordersをinner joinで結合します。
|
53
|
+
inner_join(:has_many => :orders) do
|
54
|
+
|
55
|
+
# orders.product_idの条件。:operatorで:INを指定できます。
|
56
|
+
# :attrで指定している:product_idsがUserFinderForm1にattr_accessorで宣言されます。
|
57
|
+
column(:product_id, :attr => :product_ids, :operator => :IN)
|
58
|
+
|
59
|
+
# ordersに関連するusersに関する条件を加えるため、usersをinner joinで結合します。
|
60
|
+
# このようにjoinのネストも可能です。
|
61
|
+
inner_join(:belongs_to => :product) do
|
62
|
+
|
63
|
+
# products.nameの条件。同じnameというカラムについてですが、conditionsには
|
64
|
+
# products.nameが出力され、UserFinderForm1の属性としては:product_nameが
|
65
|
+
# 宣言されます
|
66
|
+
column(:name, :attr => :product_name)
|
67
|
+
|
68
|
+
# products.priceの条件。関連に使われていないinteger,float,date,time,datetimeは
|
69
|
+
# :match => :exactなどが指定されない限りデフォルトでは範囲の条件となり、
|
70
|
+
# 属性としては :price_min, :price_maxが宣言されます
|
71
|
+
column(:price)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
実際にfindに使うコードは
|
78
|
+
form = UserFinderForm1.new(:user_name => "akimatter", :product_name => "ABC")
|
79
|
+
users = User.find(:all, form.to_find_options)
|
80
|
+
users = form.find(:all)
|
81
|
+
という感じになります。
|
82
|
+
|
83
|
+
動的にオプションを設定することも可能です。
|
84
|
+
users = User.find(:all, form.to_find_options(:include => [xxxx]))
|
85
|
+
users = form.find(:all, :include => [xxxx])
|
86
|
+
|
87
|
+
またwill_paginate用に以下のようなことも可能です。
|
88
|
+
form = UserFinderForm1.new(:user_name => "akimatter", :product_name => "ABC")
|
89
|
+
users = User.paginate(form.to_paginate_options(:page => params[:page])
|
90
|
+
users = form.paginate(:page => params[:page])
|
91
|
+
|
92
|
+
詳しくはspecを御覧下さい。
|
93
|
+
|
94
|
+
== セットアップ
|
95
|
+
=== Railsのプラグインとして
|
96
|
+
ruby script/plugin install git://github.com/akm/ar_finder_form.git
|
97
|
+
|
98
|
+
=== Railsでgemとして
|
99
|
+
まずgemcutterの設定をしていなかったら、
|
100
|
+
gem install gemcutter
|
101
|
+
gem tumble
|
102
|
+
を実行した後、
|
103
|
+
gem install ar_finder_form
|
104
|
+
|
105
|
+
で、config/initializersに以下のファイルを作成すればオッケーです。
|
106
|
+
|
107
|
+
config/initializers/ar_finder_form.rb
|
108
|
+
|
109
|
+
require 'ar_finder_form'
|
110
|
+
|
111
|
+
|
112
|
+
== 備考
|
113
|
+
フォームのクラスにはactive_formなどを継承することを想定しています。
|
114
|
+
selectable_attr(およびselectable_attr_rails)もあわせて使うと便利かも。
|
115
|
+
|
116
|
+
|
117
|
+
Copyright (c) 2008 Takeshi AKIMA, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'rspec', '>= 1.1.4'
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
require 'spec/rake/verify_rcov'
|
7
|
+
|
8
|
+
desc 'Default: run unit tests.'
|
9
|
+
task :default => :spec
|
10
|
+
|
11
|
+
task :pre_commit => [:spec, 'coverage:verify']
|
12
|
+
|
13
|
+
desc 'Run all specs under spec/**/*_spec.rb'
|
14
|
+
Spec::Rake::SpecTask.new(:spec => 'coverage:clean') do |t|
|
15
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
16
|
+
t.spec_opts = ['--options', 'spec/spec.opts']
|
17
|
+
t.rcov_dir = 'coverage'
|
18
|
+
t.rcov = true
|
19
|
+
# t.rcov_opts = ["--include-file", "lib\/*\.rb", "--exclude", "spec\/"]
|
20
|
+
t.rcov_opts = ["--exclude", "spec\/"]
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Generate documentation for the ar_finder_form plugin.'
|
24
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
25
|
+
rdoc.rdoc_dir = 'rdoc'
|
26
|
+
rdoc.title = 'FinderForm'
|
27
|
+
rdoc.options << '--line-numbers' << '--inline-source' << '-c UTF-8'
|
28
|
+
rdoc.rdoc_files.include('README*')
|
29
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
30
|
+
end
|
31
|
+
|
32
|
+
namespace :coverage do
|
33
|
+
desc "Delete aggregate coverage data."
|
34
|
+
task(:clean) { rm_f "coverage" }
|
35
|
+
|
36
|
+
desc "verify coverage threshold via RCov"
|
37
|
+
RCov::VerifyTask.new(:verify => :spec) do |t|
|
38
|
+
t.threshold = 100.0 # Make sure you have rcov 0.7 or higher!
|
39
|
+
t.index_html = 'coverage/index.html'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
begin
|
44
|
+
require 'jeweler'
|
45
|
+
Jeweler::Tasks.new do |s|
|
46
|
+
s.name = "ar_finder_form"
|
47
|
+
s.summary = "ar_finder_form provides a DSL to define form for options to find/paginate"
|
48
|
+
s.description = "ar_finder_form provides a DSL to define form for options to find/paginate"
|
49
|
+
s.email = "akima@gmail.com"
|
50
|
+
s.homepage = "http://github.com/akm/ar_finder_form/"
|
51
|
+
s.authors = ["Takeshi Akima"]
|
52
|
+
s.add_dependency("activerecord")
|
53
|
+
end
|
54
|
+
rescue LoadError
|
55
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
56
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "ar_finder_form"
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
module ArFinderForm
|
3
|
+
|
4
|
+
autoload :Config, 'ar_finder_form/config'
|
5
|
+
autoload :ClientClassMethods, 'ar_finder_form/client_class_methods'
|
6
|
+
autoload :ClientInstanceMethods, 'ar_finder_form/client_instance_methods'
|
7
|
+
autoload :Table, 'ar_finder_form/table'
|
8
|
+
autoload :JoinedTable, 'ar_finder_form/joined_table'
|
9
|
+
autoload :Column, 'ar_finder_form/column'
|
10
|
+
autoload :Attr, 'ar_finder_form/attr'
|
11
|
+
autoload :Builder, 'ar_finder_form/builder'
|
12
|
+
autoload :Context, 'ar_finder_form/context'
|
13
|
+
|
14
|
+
def self.included(mod)
|
15
|
+
mod.extend(ClientClassMethods)
|
16
|
+
mod.module_eval do
|
17
|
+
include ClientInstanceMethods
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.config
|
22
|
+
@config ||= Config.new
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'ar_finder_form'
|
2
|
+
module ArFinderForm
|
3
|
+
module Attr
|
4
|
+
autoload :Base, 'ar_finder_form/attr/base'
|
5
|
+
autoload :Static, 'ar_finder_form/attr/static'
|
6
|
+
autoload :Simple, 'ar_finder_form/attr/simple'
|
7
|
+
autoload :Like, 'ar_finder_form/attr/like'
|
8
|
+
autoload :RangeAttrs, 'ar_finder_form/attr/range_attrs'
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'ar_finder_form/attr'
|
2
|
+
module ArFinderForm
|
3
|
+
module Attr
|
4
|
+
class Base
|
5
|
+
attr_reader :column, :name, :options
|
6
|
+
attr_reader :nil_available, :array_separator
|
7
|
+
def initialize(column, name, options)
|
8
|
+
@column, @name = column, name
|
9
|
+
@nil_available = options.delete(:nil_available)
|
10
|
+
@options = options
|
11
|
+
@attr_filter = options[:attr_filter] || method(:column_type_cast)
|
12
|
+
@array_separator = options.delete(:array_separator) || /[\s\,]/
|
13
|
+
end
|
14
|
+
|
15
|
+
def table; column.table; end
|
16
|
+
def nil_available?; !!@nil_available; end
|
17
|
+
|
18
|
+
def column_name(context)
|
19
|
+
context.single_table? ? column.name : "#{table.name}.#{column.name}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def client_class_eval(script, &block)
|
23
|
+
klass = column.table.root_table.client_class
|
24
|
+
klass.module_eval(&block) if block
|
25
|
+
klass.module_eval(script) if script
|
26
|
+
end
|
27
|
+
|
28
|
+
def match?(context)
|
29
|
+
nil_available? ? true : !!context.form.send(name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def form_value(context)
|
33
|
+
result = context.form.send(name)
|
34
|
+
@attr_filter ? @attr_filter.call(result) : result
|
35
|
+
end
|
36
|
+
|
37
|
+
def form_value_array(context)
|
38
|
+
values = context.form.send(name)
|
39
|
+
values = values.to_s.split(array_separator) unless values.is_a?(Array)
|
40
|
+
@attr_filter ? values.map{|v| @attr_filter.call(v)} : values
|
41
|
+
end
|
42
|
+
|
43
|
+
def column_type_cast(value)
|
44
|
+
column.model_column.type_cast(value)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'ar_finder_form/attr'
|
2
|
+
module ArFinderForm
|
3
|
+
module Attr
|
4
|
+
class Like < Base
|
5
|
+
|
6
|
+
MATCHERS = {
|
7
|
+
:forward => '%s%%',
|
8
|
+
:backward => '%%%s',
|
9
|
+
:partial => '%%%s%%'
|
10
|
+
}
|
11
|
+
|
12
|
+
attr_reader :operator
|
13
|
+
def initialize(column, name, options)
|
14
|
+
super(column, name, options)
|
15
|
+
@mathcer = MATCHERS[options[:match]] || MATCHERS[:partial]
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup
|
19
|
+
client_class_eval("attr_accessor :#{name}")
|
20
|
+
end
|
21
|
+
|
22
|
+
def build(context)
|
23
|
+
return unless match?(context)
|
24
|
+
context.add_condition(
|
25
|
+
"#{column_name(context)} LIKE ?",
|
26
|
+
@mathcer % form_value(context).to_s)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'ar_finder_form/attr'
|
2
|
+
module ArFinderForm
|
3
|
+
module Attr
|
4
|
+
class RangeAttrs < Base
|
5
|
+
|
6
|
+
attr_accessor :min, :max
|
7
|
+
def initialize(column, name, options)
|
8
|
+
super(column, name, options)
|
9
|
+
range = options[:range] || {}
|
10
|
+
min_def = {:operator => '>='}.update(range[:min] || options[:min] || {})
|
11
|
+
max_def = {:operator => '<='}.update(range[:max] || options[:max] || {})
|
12
|
+
@min = Simple.new(column, min_def[:attr] || "#{name}_min", min_def)
|
13
|
+
@max = Simple.new(column, max_def[:attr] || "#{name}_max", max_def)
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup
|
17
|
+
@min.setup
|
18
|
+
@max.setup
|
19
|
+
end
|
20
|
+
|
21
|
+
def build(context)
|
22
|
+
sub_context = context.new_sub_context(:connector => 'AND')
|
23
|
+
@min.build(sub_context)
|
24
|
+
@max.build(sub_context)
|
25
|
+
context.merge(sub_context)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'ar_finder_form/attr'
|
2
|
+
module ArFinderForm
|
3
|
+
module Attr
|
4
|
+
class Simple < Base
|
5
|
+
attr_reader :operator
|
6
|
+
def initialize(column, name, options)
|
7
|
+
super(column, name, options)
|
8
|
+
@operator = options.delete(:operator).to_s.downcase.to_sym
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup
|
12
|
+
client_class_eval("attr_accessor :#{name}")
|
13
|
+
end
|
14
|
+
|
15
|
+
def build(context)
|
16
|
+
return unless match?(context)
|
17
|
+
case operator
|
18
|
+
when :in then
|
19
|
+
context.add_condition("#{column_name(context)} IN (?)",
|
20
|
+
form_value_array(context))
|
21
|
+
else
|
22
|
+
context.add_condition(
|
23
|
+
"#{column_name(context)} #{operator} ?",
|
24
|
+
form_value(context))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'ar_finder_form/attr'
|
2
|
+
module ArFinderForm
|
3
|
+
module Attr
|
4
|
+
class Static < Base
|
5
|
+
|
6
|
+
attr_reader :values
|
7
|
+
def initialize(column, name, values, options)
|
8
|
+
super(column, name, options)
|
9
|
+
@values = values || {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def setup
|
13
|
+
# do nothing
|
14
|
+
end
|
15
|
+
|
16
|
+
def build(context)
|
17
|
+
context.add_condition(
|
18
|
+
values.map{|v| "#{column_name(context)} #{v}"}.
|
19
|
+
join(' %s ' % options[:connector]))
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'ar_finder_form'
|
2
|
+
|
3
|
+
module ArFinderForm
|
4
|
+
class Builder < Table
|
5
|
+
attr_reader :client_class, :model_class
|
6
|
+
attr_reader :columns
|
7
|
+
def initialize(client_class, model_class)
|
8
|
+
super(model_class)
|
9
|
+
@client_class = client_class
|
10
|
+
end
|
11
|
+
|
12
|
+
def root_table
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def build(context)
|
17
|
+
context.single_table = joined_tables.empty?
|
18
|
+
super(context)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'ar_finder_form'
|
2
|
+
module ArFinderForm
|
3
|
+
module ClientClassMethods
|
4
|
+
attr_reader :builder
|
5
|
+
def with_model(model_class, &block)
|
6
|
+
@builder = Builder.new(self, model_class)
|
7
|
+
@builder.instance_eval(&block)
|
8
|
+
@builder.build_methods
|
9
|
+
@builder
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_options(value = nil)
|
13
|
+
@find_options = value if value
|
14
|
+
@find_options ||= {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def paginate_options(value = nil)
|
18
|
+
@find_options = value if value
|
19
|
+
@find_options ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|