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 ADDED
@@ -0,0 +1,2 @@
1
+ /coverage
2
+ /pkg
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