meta_search_hub 1.1.3
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 +7 -0
- data/.document +5 -0
- data/.gitmodules +6 -0
- data/CHANGELOG +109 -0
- data/Gemfile +12 -0
- data/LICENSE +20 -0
- data/README.rdoc +438 -0
- data/Rakefile +69 -0
- data/VERSION +1 -0
- data/lib/meta_search/builder.rb +316 -0
- data/lib/meta_search/exceptions.rb +17 -0
- data/lib/meta_search/helpers/form_builder.rb +166 -0
- data/lib/meta_search/helpers/form_helper.rb +24 -0
- data/lib/meta_search/helpers/url_helper.rb +66 -0
- data/lib/meta_search/helpers.rb +3 -0
- data/lib/meta_search/locale/en.yml +24 -0
- data/lib/meta_search/method.rb +129 -0
- data/lib/meta_search/model_compatibility.rb +75 -0
- data/lib/meta_search/searches/active_record.rb +203 -0
- data/lib/meta_search/utility.rb +110 -0
- data/lib/meta_search/where.rb +264 -0
- data/lib/meta_search.rb +58 -0
- data/meta_search.gemspec +90 -0
- data/test/fixtures/companies.yml +17 -0
- data/test/fixtures/company.rb +22 -0
- data/test/fixtures/data_type.rb +5 -0
- data/test/fixtures/data_types.yml +15 -0
- data/test/fixtures/developer.rb +11 -0
- data/test/fixtures/developers.yml +62 -0
- data/test/fixtures/developers_projects.yml +25 -0
- data/test/fixtures/note.rb +3 -0
- data/test/fixtures/notes.yml +79 -0
- data/test/fixtures/project.rb +4 -0
- data/test/fixtures/projects.yml +34 -0
- data/test/fixtures/schema.rb +47 -0
- data/test/helper.rb +44 -0
- data/test/locales/es.yml +5 -0
- data/test/locales/flanders.yml +18 -0
- data/test/test_search.rb +969 -0
- data/test/test_view_helpers.rb +447 -0
- metadata +160 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require 'meta_search/utility'
|
|
2
|
+
|
|
3
|
+
module MetaSearch
|
|
4
|
+
# MetaSearch can be given access to any class method on your model to extend its search capabilities.
|
|
5
|
+
# The only rule is that the method must return an ActiveRecord::Relation so that MetaSearch can
|
|
6
|
+
# continue to extend the search with other attributes. Conveniently, scopes (formerly "named scopes")
|
|
7
|
+
# do this already.
|
|
8
|
+
#
|
|
9
|
+
# Consider the following model:
|
|
10
|
+
#
|
|
11
|
+
# class Company < ActiveRecord::Base
|
|
12
|
+
# has_many :slackers, :class_name => "Developer", :conditions => {:slacker => true}
|
|
13
|
+
# scope :backwards_name, lambda {|name| where(:name => name.reverse)}
|
|
14
|
+
# scope :with_slackers_by_name_and_salary_range,
|
|
15
|
+
# lambda {|name, low, high|
|
|
16
|
+
# joins(:slackers).where(:developers => {:name => name, :salary => low..high})
|
|
17
|
+
# }
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# To allow MetaSearch access to a model method, including a named scope, just use
|
|
21
|
+
# <tt>search_methods</tt> in the model:
|
|
22
|
+
#
|
|
23
|
+
# search_methods :backwards_name
|
|
24
|
+
#
|
|
25
|
+
# This will allow you to add a text field named :backwards_name to your search form, and
|
|
26
|
+
# it will behave as you might expect.
|
|
27
|
+
#
|
|
28
|
+
# In the case of the second scope, we have multiple parameters to pass in, of different
|
|
29
|
+
# types. We can pass the following to <tt>search_methods</tt>:
|
|
30
|
+
#
|
|
31
|
+
# search_methods :with_slackers_by_name_and_salary_range,
|
|
32
|
+
# :splat_param => true, :type => [:string, :integer, :integer]
|
|
33
|
+
#
|
|
34
|
+
# MetaSearch needs us to tell it that we don't want to keep the array supplied to it as-is, but
|
|
35
|
+
# "splat" it when passing it to the model method. And in this case, ActiveRecord would have been
|
|
36
|
+
# smart enough to handle the typecasting for us, but I wanted to demonstrate how we can tell
|
|
37
|
+
# MetaSearch that a given parameter is of a specific database "column type." This is just a hint
|
|
38
|
+
# MetaSearch uses in the same way it does when casting "Where" params based on the DB column
|
|
39
|
+
# being searched. It's also important so that things like dates get handled properly by FormBuilder.
|
|
40
|
+
#
|
|
41
|
+
# _NOTE_: If you do supply an array, rather than a single type value, to <tt>:type</tt>, MetaSearch
|
|
42
|
+
# will enforce that any array supplied for input by your forms has the correct number of elements
|
|
43
|
+
# for your eventual method.
|
|
44
|
+
#
|
|
45
|
+
# Besides <tt>:splat_param</tt> and <tt>:type</tt>, search_methods accept the same <tt>:formatter</tt>
|
|
46
|
+
# and <tt>:validator</tt> options that you would use when adding a new MetaSearch::Where:
|
|
47
|
+
#
|
|
48
|
+
# <tt>formatter</tt> is the Proc that will do any formatting to the variable passed to your method.
|
|
49
|
+
# The default proc is <tt>{|param| param}</tt>, which doesn't really do anything. If you pass a
|
|
50
|
+
# string, it will be +eval+ed in the context of this Proc.
|
|
51
|
+
#
|
|
52
|
+
# If your method will do a LIKE search against its parameter, you might want to pass:
|
|
53
|
+
#
|
|
54
|
+
# :formatter => '"%#{param}%"'
|
|
55
|
+
#
|
|
56
|
+
# Be sure to single-quote the string, so that variables aren't interpolated until later. If in doubt,
|
|
57
|
+
# just use a Proc, like so:
|
|
58
|
+
#
|
|
59
|
+
# :formatter => Proc.new {|param| "%#{param}%"}
|
|
60
|
+
#
|
|
61
|
+
# <tt>validator</tt> is the Proc that will be used to check whether a parameter supplied to the
|
|
62
|
+
# method is valid. If it is not valid, it won't be used in the query. The default is
|
|
63
|
+
# <tt>{|param| !param.blank?}</tt>, so that empty parameters aren't added to the search, but you
|
|
64
|
+
# can get more complex if you desire. Validations are run after typecasting, so you can check
|
|
65
|
+
# the class of your parameters, for instance.
|
|
66
|
+
class Method
|
|
67
|
+
include Utility
|
|
68
|
+
|
|
69
|
+
attr_reader :name, :formatter, :validator, :type
|
|
70
|
+
|
|
71
|
+
def initialize(name, opts ={})
|
|
72
|
+
raise ArgumentError, "Name parameter required" if name.blank?
|
|
73
|
+
@name = name
|
|
74
|
+
@type = opts[:type] || :string
|
|
75
|
+
@splat_param = opts[:splat_param] || false
|
|
76
|
+
@formatter = opts[:formatter] || Proc.new {|param| param}
|
|
77
|
+
if @formatter.is_a?(String)
|
|
78
|
+
formatter = @formatter
|
|
79
|
+
@formatter = Proc.new {|param| eval formatter}
|
|
80
|
+
end
|
|
81
|
+
unless @formatter.respond_to?(:call)
|
|
82
|
+
raise ArgumentError, "Invalid formatter for #{name}, should be a Proc or String."
|
|
83
|
+
end
|
|
84
|
+
@validator = opts[:validator] || Proc.new {|param| !param.blank?}
|
|
85
|
+
unless @validator.respond_to?(:call)
|
|
86
|
+
raise ArgumentError, "Invalid validator for #{name}, should be a Proc."
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Cast the parameter to the type specified in the Method's <tt>type</tt>
|
|
91
|
+
def cast_param(param)
|
|
92
|
+
if type.is_a?(Array)
|
|
93
|
+
unless param.is_a?(Array) && param.size == type.size
|
|
94
|
+
num_params = param.is_a?(Array) ? param.size : 1
|
|
95
|
+
raise ArgumentError, "Parameters supplied to #{name} could not be type cast -- #{num_params} values supplied, #{type.size} expected"
|
|
96
|
+
end
|
|
97
|
+
type.each_with_index do |t, i|
|
|
98
|
+
param[i] = cast_attributes(t, param[i])
|
|
99
|
+
end
|
|
100
|
+
param
|
|
101
|
+
else
|
|
102
|
+
cast_attributes(type, param)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Evaluate the method in the context of the supplied relation and parameter
|
|
107
|
+
def evaluate(relation, param)
|
|
108
|
+
if splat_param?
|
|
109
|
+
relation.send(name, *format_param(param))
|
|
110
|
+
else
|
|
111
|
+
relation.send(name, format_param(param))
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def splat_param?
|
|
116
|
+
!!@splat_param
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Format a parameter for searching using the Method's defined formatter.
|
|
120
|
+
def format_param(param)
|
|
121
|
+
formatter.call(param)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Validate the parameter for use in a search using the Method's defined validator.
|
|
125
|
+
def validate(param)
|
|
126
|
+
validator.call(param)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'meta_search/utility'
|
|
2
|
+
|
|
3
|
+
module MetaSearch
|
|
4
|
+
|
|
5
|
+
module ModelCompatibility
|
|
6
|
+
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.extend ClassMethods
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def persisted?
|
|
12
|
+
false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_key
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_param
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_model
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Name < String
|
|
29
|
+
attr_reader :singular, :plural, :element, :collection, :partial_path, :human, :param_key, :route_key, :i18n_key
|
|
30
|
+
alias_method :cache_key, :collection
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
super("Search")
|
|
34
|
+
@singular = "search".freeze
|
|
35
|
+
@plural = "searches".freeze
|
|
36
|
+
@element = "search".freeze
|
|
37
|
+
@human = "Search".freeze
|
|
38
|
+
@collection = "meta_search/searches".freeze
|
|
39
|
+
@partial_path = "#{@collection}/#{@element}".freeze
|
|
40
|
+
@param_key = "search".freeze
|
|
41
|
+
@route_key = "searches".freeze
|
|
42
|
+
@i18n_key = :meta_search
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module ClassMethods
|
|
47
|
+
include Utility
|
|
48
|
+
|
|
49
|
+
def model_name
|
|
50
|
+
@_model_name ||= Name.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def human_attribute_name(attribute, options = {})
|
|
54
|
+
method_name = preferred_method_name(attribute)
|
|
55
|
+
|
|
56
|
+
defaults = [:"meta_search.attributes.#{klass.model_name.i18n_key}.#{method_name || attribute}"]
|
|
57
|
+
|
|
58
|
+
if method_name
|
|
59
|
+
predicate = Where.get(method_name)[:name]
|
|
60
|
+
predicate_attribute = method_name.sub(/_#{predicate}=?$/, '')
|
|
61
|
+
predicate_attributes = predicate_attribute.split(/_or_/).map { |att|
|
|
62
|
+
klass.human_attribute_name(att)
|
|
63
|
+
}.join(" #{I18n.translate(:"meta_search.or", :default => 'or')} ")
|
|
64
|
+
defaults << :"meta_search.predicates.#{predicate}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
defaults << options.delete(:default) if options[:default]
|
|
68
|
+
defaults << attribute.to_s.humanize
|
|
69
|
+
|
|
70
|
+
options.reverse_merge! :count => 1, :default => defaults, :attribute => predicate_attributes || klass.human_attribute_name(attribute)
|
|
71
|
+
I18n.translate(defaults.shift, options)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
require 'active_support/concern'
|
|
2
|
+
require 'meta_search/method'
|
|
3
|
+
require 'meta_search/builder'
|
|
4
|
+
|
|
5
|
+
module MetaSearch
|
|
6
|
+
module Searches
|
|
7
|
+
|
|
8
|
+
module ActiveRecord
|
|
9
|
+
|
|
10
|
+
def self.included(base)
|
|
11
|
+
base.extend ClassMethods
|
|
12
|
+
|
|
13
|
+
base.class_eval do
|
|
14
|
+
class_attribute :_metasearch_include_attributes, :_metasearch_exclude_attributes
|
|
15
|
+
class_attribute :_metasearch_include_associations, :_metasearch_exclude_associations
|
|
16
|
+
class_attribute :_metasearch_methods
|
|
17
|
+
self._metasearch_include_attributes =
|
|
18
|
+
self._metasearch_exclude_attributes =
|
|
19
|
+
self._metasearch_exclude_associations =
|
|
20
|
+
self._metasearch_include_associations = {}
|
|
21
|
+
self._metasearch_methods = {}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module ClassMethods
|
|
26
|
+
# Prepares the search to run against your model. Returns an instance of
|
|
27
|
+
# MetaSearch::Builder, which behaves pretty much like an ActiveRecord::Relation,
|
|
28
|
+
# in that it doesn't actually query the database until you do something that
|
|
29
|
+
# requires it to do so.
|
|
30
|
+
#
|
|
31
|
+
# Options:
|
|
32
|
+
#
|
|
33
|
+
# * +params+ - a hash of valid searches with keys that are valid according to
|
|
34
|
+
# the docs in MetaSearch::Where.
|
|
35
|
+
# * +opts+ - A hash of additional information that will be passed through to
|
|
36
|
+
# the search's Builder object. +search_key+, if present, will override the
|
|
37
|
+
# default param name, 'search', in any sort_links generated by this Builder.
|
|
38
|
+
# All other keys are passed untouched to the builder, and available from the
|
|
39
|
+
# Builder's +options+ reader for use in :if blocks supplied to attr_searchable
|
|
40
|
+
# and friends.
|
|
41
|
+
def metasearch(params = nil, opts = nil)
|
|
42
|
+
builder = Searches.for(self).new(self, opts || {})
|
|
43
|
+
builder.build(params || {})
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
alias_method :search, :metasearch unless respond_to?(:search)
|
|
47
|
+
|
|
48
|
+
def _metasearch_method_authorized?(name, metasearch_object)
|
|
49
|
+
name = name.to_s
|
|
50
|
+
meth = self._metasearch_methods[name]
|
|
51
|
+
meth && (meth[:if] ? meth[:if].call(metasearch_object) : true)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def _metasearch_attribute_authorized?(name, metasearch_object)
|
|
55
|
+
name = name.to_s
|
|
56
|
+
if self._metasearch_include_attributes.empty?
|
|
57
|
+
!_metasearch_excludes_attribute?(name, metasearch_object)
|
|
58
|
+
else
|
|
59
|
+
_metasearch_includes_attribute?(name, metasearch_object)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def _metasearch_association_authorized?(name, metasearch_object)
|
|
64
|
+
name = name.to_s
|
|
65
|
+
if self._metasearch_include_associations.empty?
|
|
66
|
+
!_metasearch_excludes_association?(name, metasearch_object)
|
|
67
|
+
else
|
|
68
|
+
_metasearch_includes_association?(name, metasearch_object)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Excludes model attributes from searchability. This means that searches can't be created against
|
|
75
|
+
# these columns, whether the search is based on this model, or the model's attributes are being
|
|
76
|
+
# searched by association from another model. If a Comment <tt>belongs_to :article</tt> but declares
|
|
77
|
+
# <tt>attr_unsearchable :user_id</tt> then <tt>Comment.search</tt> won't accept parameters
|
|
78
|
+
# like <tt>:user_id_equals</tt>, nor will an Article.search accept the parameter
|
|
79
|
+
# <tt>:comments_user_id_equals</tt>.
|
|
80
|
+
def attr_unsearchable(*args)
|
|
81
|
+
if table_exists?
|
|
82
|
+
opts = args.extract_options!
|
|
83
|
+
args.flatten.each do |attr|
|
|
84
|
+
attr = attr.to_s
|
|
85
|
+
raise(ArgumentError, "No persisted attribute (column) named #{attr} in #{self}") unless self.columns_hash.has_key?(attr)
|
|
86
|
+
self._metasearch_exclude_attributes = self._metasearch_exclude_attributes.merge(
|
|
87
|
+
attr => {
|
|
88
|
+
:if => opts[:if]
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Like <tt>attr_unsearchable</tt>, but operates as a whitelist rather than blacklist. If both
|
|
96
|
+
# <tt>attr_searchable</tt> and <tt>attr_unsearchable</tt> are present, the latter
|
|
97
|
+
# is ignored.
|
|
98
|
+
def attr_searchable(*args)
|
|
99
|
+
if table_exists?
|
|
100
|
+
opts = args.extract_options!
|
|
101
|
+
args.flatten.each do |attr|
|
|
102
|
+
attr = attr.to_s
|
|
103
|
+
raise(ArgumentError, "No persisted attribute (column) named #{attr} in #{self}") unless self.columns_hash.has_key?(attr)
|
|
104
|
+
self._metasearch_include_attributes = self._metasearch_include_attributes.merge(
|
|
105
|
+
attr => {
|
|
106
|
+
:if => opts[:if]
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Excludes model associations from searchability. This mean that searches can't be created against
|
|
114
|
+
# these associations. An article that <tt>has_many :comments</tt> but excludes comments from
|
|
115
|
+
# searching by declaring <tt>assoc_unsearchable :comments</tt> won't make any of the
|
|
116
|
+
# <tt>comments_*</tt> methods available.
|
|
117
|
+
def assoc_unsearchable(*args)
|
|
118
|
+
opts = args.extract_options!
|
|
119
|
+
args.flatten.each do |assoc|
|
|
120
|
+
assoc = assoc.to_s
|
|
121
|
+
raise(ArgumentError, "No such association #{assoc} in #{self}") unless self.reflect_on_all_associations.map {|a| a.name.to_s}.include?(assoc)
|
|
122
|
+
self._metasearch_exclude_associations = self._metasearch_exclude_associations.merge(
|
|
123
|
+
assoc => {
|
|
124
|
+
:if => opts[:if]
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# As with <tt>attr_searchable</tt> this is the whitelist version of
|
|
131
|
+
# <tt>assoc_unsearchable</tt>
|
|
132
|
+
def assoc_searchable(*args)
|
|
133
|
+
opts = args.extract_options!
|
|
134
|
+
args.flatten.each do |assoc|
|
|
135
|
+
assoc = assoc.to_s
|
|
136
|
+
raise(ArgumentError, "No such association #{assoc} in #{self}") unless self.reflect_on_all_associations.map {|a| a.name.to_s}.include?(assoc)
|
|
137
|
+
self._metasearch_include_associations = self._metasearch_include_associations.merge(
|
|
138
|
+
assoc => {
|
|
139
|
+
:if => opts[:if]
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def search_methods(*args)
|
|
146
|
+
opts = args.extract_options!
|
|
147
|
+
authorizer = opts.delete(:if)
|
|
148
|
+
args.flatten.map(&:to_s).each do |arg|
|
|
149
|
+
self._metasearch_methods = self._metasearch_methods.merge(
|
|
150
|
+
arg => {
|
|
151
|
+
:method => MetaSearch::Method.new(arg, opts),
|
|
152
|
+
:if => authorizer
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
alias_method :search_method, :search_methods
|
|
159
|
+
|
|
160
|
+
def _metasearch_includes_attribute?(name, metasearch_object)
|
|
161
|
+
attr = self._metasearch_include_attributes[name]
|
|
162
|
+
attr && (attr[:if] ? attr[:if].call(metasearch_object) : true)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def _metasearch_excludes_attribute?(name, metasearch_object)
|
|
166
|
+
attr = self._metasearch_exclude_attributes[name]
|
|
167
|
+
attr && (attr[:if] ? attr[:if].call(metasearch_object) : true)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def _metasearch_includes_association?(name, metasearch_object)
|
|
171
|
+
assoc = self._metasearch_include_associations[name]
|
|
172
|
+
assoc && (assoc[:if] ? assoc[:if].call(metasearch_object) : true)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def _metasearch_excludes_association?(name, metasearch_object)
|
|
176
|
+
assoc = self._metasearch_exclude_associations[name]
|
|
177
|
+
assoc && (assoc[:if] ? assoc[:if].call(metasearch_object) : true)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def self.for(klass)
|
|
184
|
+
DISPATCH[klass.name]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
DISPATCH = Hash.new do |hash, klass_name|
|
|
190
|
+
class_name = klass_name.gsub('::', '_')
|
|
191
|
+
hash[klass_name] = module_eval <<-RUBY_EVAL
|
|
192
|
+
class #{class_name} < MetaSearch::Builder
|
|
193
|
+
def self.klass
|
|
194
|
+
::#{klass_name}
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
#{class_name}
|
|
199
|
+
RUBY_EVAL
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require 'meta_search/exceptions'
|
|
2
|
+
|
|
3
|
+
module MetaSearch
|
|
4
|
+
module Utility #:nodoc:
|
|
5
|
+
|
|
6
|
+
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
|
|
7
|
+
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def preferred_method_name(method_id)
|
|
12
|
+
method_name = method_id.to_s
|
|
13
|
+
where = Where.new(method_name) rescue nil
|
|
14
|
+
return nil unless where
|
|
15
|
+
where.aliases.each do |a|
|
|
16
|
+
break if method_name.sub!(/#{a}(=?)$/, "#{where.name}\\1")
|
|
17
|
+
end
|
|
18
|
+
method_name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def array_of_strings?(o)
|
|
22
|
+
o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def array_of_arrays?(vals)
|
|
26
|
+
vals.is_a?(Array) && vals.first.is_a?(Array)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def array_of_dates?(vals)
|
|
30
|
+
vals.is_a?(Array) && vals.first.respond_to?(:to_time)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cast_attributes(type, vals)
|
|
34
|
+
if array_of_arrays?(vals)
|
|
35
|
+
vals.map! {|v| cast_attributes(type, v)}
|
|
36
|
+
# Need to make sure not to kill multiparam dates/times
|
|
37
|
+
elsif vals.is_a?(Array) && (array_of_dates?(vals) || !(DATES+TIMES).include?(type))
|
|
38
|
+
vals.map! {|v| cast_attribute(type, v)}
|
|
39
|
+
else
|
|
40
|
+
cast_attribute(type, vals)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def cast_attribute(type, val)
|
|
45
|
+
case type
|
|
46
|
+
when *STRINGS
|
|
47
|
+
val.respond_to?(:to_s) ? val.to_s : String.new(val)
|
|
48
|
+
when *DATES
|
|
49
|
+
if val.respond_to?(:to_date)
|
|
50
|
+
val.to_date rescue nil
|
|
51
|
+
else
|
|
52
|
+
y, m, d = *[val].flatten
|
|
53
|
+
m ||= 1
|
|
54
|
+
d ||= 1
|
|
55
|
+
Date.new(y,m,d) rescue nil
|
|
56
|
+
end
|
|
57
|
+
when *TIMES
|
|
58
|
+
if val.is_a?(Array)
|
|
59
|
+
y, m, d, hh, mm, ss = *[val].flatten
|
|
60
|
+
Time.zone.local(y, m, d, hh, mm, ss) rescue nil
|
|
61
|
+
else
|
|
62
|
+
unless val.acts_like?(:time)
|
|
63
|
+
val = val.is_a?(String) ? Time.zone.parse(val) : val.to_time rescue val
|
|
64
|
+
end
|
|
65
|
+
val.in_time_zone rescue nil
|
|
66
|
+
end
|
|
67
|
+
when *BOOLEANS
|
|
68
|
+
if val.is_a?(String) && val.blank?
|
|
69
|
+
nil
|
|
70
|
+
else
|
|
71
|
+
TRUE_VALUES.include?(val)
|
|
72
|
+
end
|
|
73
|
+
when :integer
|
|
74
|
+
val.blank? ? nil : val.to_i
|
|
75
|
+
when :float
|
|
76
|
+
val.blank? ? nil : val.to_f
|
|
77
|
+
when :decimal
|
|
78
|
+
if val.blank?
|
|
79
|
+
nil
|
|
80
|
+
elsif val.class == BigDecimal
|
|
81
|
+
val
|
|
82
|
+
elsif val.respond_to?(:to_d)
|
|
83
|
+
val.to_d
|
|
84
|
+
else
|
|
85
|
+
val.to_s.to_d
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
raise TypeCastError, "Unable to cast columns of type #{type}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def collapse_multiparameter_options(opts)
|
|
93
|
+
opts.keys.each do |k|
|
|
94
|
+
if k.include?("(")
|
|
95
|
+
real_attribute, position = k.split(/\(|\)/)
|
|
96
|
+
cast = %w(a s i).include?(position.last) ? position.last : nil
|
|
97
|
+
position = position.to_i - 1
|
|
98
|
+
value = opts.delete(k)
|
|
99
|
+
opts[real_attribute] ||= []
|
|
100
|
+
opts[real_attribute][position] = if cast
|
|
101
|
+
(value.blank? && cast == 'i') ? nil : value.send("to_#{cast}")
|
|
102
|
+
else
|
|
103
|
+
value
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
opts
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|