sequel_core 1.5.1 → 2.0.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 (68) hide show
  1. data/CHANGELOG +116 -0
  2. data/COPYING +19 -19
  3. data/README +83 -32
  4. data/Rakefile +9 -20
  5. data/bin/sequel +43 -112
  6. data/doc/cheat_sheet.rdoc +225 -0
  7. data/doc/dataset_filtering.rdoc +257 -0
  8. data/lib/sequel_core/adapters/adapter_skeleton.rb +4 -2
  9. data/lib/sequel_core/adapters/ado.rb +3 -1
  10. data/lib/sequel_core/adapters/db2.rb +4 -2
  11. data/lib/sequel_core/adapters/dbi.rb +127 -113
  12. data/lib/sequel_core/adapters/informix.rb +4 -2
  13. data/lib/sequel_core/adapters/jdbc.rb +5 -3
  14. data/lib/sequel_core/adapters/mysql.rb +112 -46
  15. data/lib/sequel_core/adapters/odbc.rb +5 -7
  16. data/lib/sequel_core/adapters/odbc_mssql.rb +12 -3
  17. data/lib/sequel_core/adapters/openbase.rb +3 -1
  18. data/lib/sequel_core/adapters/oracle.rb +11 -9
  19. data/lib/sequel_core/adapters/postgres.rb +261 -262
  20. data/lib/sequel_core/adapters/sqlite.rb +72 -22
  21. data/lib/sequel_core/connection_pool.rb +140 -73
  22. data/lib/sequel_core/core_ext.rb +201 -66
  23. data/lib/sequel_core/core_sql.rb +123 -153
  24. data/lib/sequel_core/database/schema.rb +156 -0
  25. data/lib/sequel_core/database.rb +321 -338
  26. data/lib/sequel_core/dataset/callback.rb +11 -12
  27. data/lib/sequel_core/dataset/convenience.rb +213 -240
  28. data/lib/sequel_core/dataset/pagination.rb +58 -43
  29. data/lib/sequel_core/dataset/parse_tree_sequelizer.rb +331 -0
  30. data/lib/sequel_core/dataset/query.rb +41 -0
  31. data/lib/sequel_core/dataset/schema.rb +15 -0
  32. data/lib/sequel_core/dataset/sequelizer.rb +41 -373
  33. data/lib/sequel_core/dataset/sql.rb +741 -632
  34. data/lib/sequel_core/dataset.rb +183 -168
  35. data/lib/sequel_core/deprecated.rb +1 -169
  36. data/lib/sequel_core/exceptions.rb +24 -19
  37. data/lib/sequel_core/migration.rb +44 -52
  38. data/lib/sequel_core/object_graph.rb +43 -42
  39. data/lib/sequel_core/pretty_table.rb +71 -76
  40. data/lib/sequel_core/schema/generator.rb +163 -105
  41. data/lib/sequel_core/schema/sql.rb +250 -93
  42. data/lib/sequel_core/schema.rb +2 -8
  43. data/lib/sequel_core/sql.rb +394 -0
  44. data/lib/sequel_core/worker.rb +37 -27
  45. data/lib/sequel_core.rb +99 -45
  46. data/spec/adapters/informix_spec.rb +0 -1
  47. data/spec/adapters/mysql_spec.rb +177 -124
  48. data/spec/adapters/oracle_spec.rb +0 -1
  49. data/spec/adapters/postgres_spec.rb +98 -58
  50. data/spec/adapters/sqlite_spec.rb +45 -4
  51. data/spec/blockless_filters_spec.rb +269 -0
  52. data/spec/connection_pool_spec.rb +21 -18
  53. data/spec/core_ext_spec.rb +169 -19
  54. data/spec/core_sql_spec.rb +56 -49
  55. data/spec/database_spec.rb +78 -17
  56. data/spec/dataset_spec.rb +300 -428
  57. data/spec/migration_spec.rb +1 -1
  58. data/spec/object_graph_spec.rb +5 -11
  59. data/spec/rcov.opts +1 -1
  60. data/spec/schema_generator_spec.rb +16 -4
  61. data/spec/schema_spec.rb +89 -10
  62. data/spec/sequelizer_spec.rb +56 -56
  63. data/spec/spec.opts +0 -5
  64. data/spec/spec_config.rb +7 -0
  65. data/spec/spec_config.rb.example +5 -5
  66. data/spec/spec_helper.rb +6 -0
  67. data/spec/worker_spec.rb +1 -1
  68. metadata +78 -63
@@ -10,9 +10,7 @@ module Sequel
10
10
  end
11
11
 
12
12
  def connect
13
- if @opts[:database].nil? || @opts[:database].empty?
14
- @opts[:database] = ':memory:'
15
- end
13
+ @opts[:database] = ':memory:' if @opts[:database].blank?
16
14
  db = ::SQLite3::Database.new(@opts[:database])
17
15
  db.busy_timeout(@opts.fetch(:timeout, 5000))
18
16
  db.type_translation = true
@@ -38,31 +36,39 @@ module Sequel
38
36
  end
39
37
 
40
38
  def execute(sql)
41
- @logger.info(sql) if @logger
42
- @pool.hold {|conn| conn.execute_batch(sql); conn.changes}
43
- rescue RuntimeError => e
44
- raise Error::InvalidStatement, "#{sql}\r\n#{e.message}"
39
+ begin
40
+ log_info(sql)
41
+ @pool.hold {|conn| conn.execute_batch(sql); conn.changes}
42
+ rescue SQLite3::Exception => e
43
+ raise Error::InvalidStatement, "#{sql}\r\n#{e.message}"
44
+ end
45
45
  end
46
46
 
47
47
  def execute_insert(sql)
48
- @logger.info(sql) if @logger
49
- @pool.hold {|conn| conn.execute(sql); conn.last_insert_row_id}
50
- rescue RuntimeError => e
51
- raise Error::InvalidStatement, "#{sql}\r\n#{e.message}"
48
+ begin
49
+ log_info(sql)
50
+ @pool.hold {|conn| conn.execute(sql); conn.last_insert_row_id}
51
+ rescue SQLite3::Exception => e
52
+ raise Error::InvalidStatement, "#{sql}\r\n#{e.message}"
53
+ end
52
54
  end
53
55
 
54
56
  def single_value(sql)
55
- @logger.info(sql) if @logger
56
- @pool.hold {|conn| conn.get_first_value(sql)}
57
- rescue RuntimeError => e
58
- raise Error::InvalidStatement, "#{sql}\r\n#{e.message}"
57
+ begin
58
+ log_info(sql)
59
+ @pool.hold {|conn| conn.get_first_value(sql)}
60
+ rescue SQLite3::Exception => e
61
+ raise Error::InvalidStatement, "#{sql}\r\n#{e.message}"
62
+ end
59
63
  end
60
64
 
61
65
  def execute_select(sql, &block)
62
- @logger.info(sql) if @logger
63
- @pool.hold {|conn| conn.query(sql, &block)}
64
- rescue RuntimeError => e
65
- raise Error::InvalidStatement, "#{sql}\r\n#{e.message}"
66
+ begin
67
+ log_info(sql)
68
+ @pool.hold {|conn| conn.query(sql, &block)}
69
+ rescue SQLite3::Exception => e
70
+ raise Error::InvalidStatement, "#{sql}\r\n#{e.message}"
71
+ end
66
72
  end
67
73
 
68
74
  def pragma_get(name)
@@ -124,20 +130,64 @@ module Sequel
124
130
  result = nil
125
131
  conn.transaction {result = yield(conn)}
126
132
  result
127
- rescue => e
128
- raise e unless Error::Rollback === e
133
+ rescue ::Exception => e
134
+ raise (SQLite3::Exception === e ? Error.new(e.message) : e) unless Error::Rollback === e
135
+ end
136
+ end
137
+ end
138
+
139
+ private
140
+ def connection_pool_default_options
141
+ o = super.merge(:pool_reuse_connections=>:always, :pool_convert_exceptions=>false)
142
+ # Default to only a single connection if a memory database is used,
143
+ # because otherwise each connection will get a separate database
144
+ o[:max_connections] = 1 if @opts[:database] == ':memory:' || @opts[:database].blank?
145
+ o
146
+ end
147
+
148
+ SCHEMA_TYPE_RE = /\A(\w+)\((\d+)\)\z/
149
+ def schema_parse_table(table_name, opts)
150
+ rows = self["PRAGMA table_info('#{::SQLite3::Database.quote(table_name.to_s)}')"].collect do |row|
151
+ row.delete(:cid)
152
+ row[:column] = row.delete(:name)
153
+ row[:allow_null] = row.delete(:notnull).to_i == 0 ? 'YES' : 'NO'
154
+ row[:default] = row.delete(:dflt_value)
155
+ row[:primary_key] = row.delete(:pk).to_i == 1 ? true : false
156
+ row[:db_type] = row.delete(:type)
157
+ if m = SCHEMA_TYPE_RE.match(row[:db_type])
158
+ row[:db_type] = m[1]
159
+ row[:max_chars] = m[2].to_i
160
+ else
161
+ row[:max_chars] = nil
129
162
  end
163
+ row[:numeric_precision] = nil
164
+ row
130
165
  end
166
+ schema_parse_rows(rows)
167
+ end
168
+
169
+ def schema_parse_tables(opts)
170
+ schemas = {}
171
+ tables.each{|table| schemas[table] = schema_parse_table(table, opts)}
172
+ schemas
131
173
  end
132
174
  end
133
175
 
134
176
  class Dataset < Sequel::Dataset
135
- def quote_column_ref(c); "`#{c}`"; end
177
+ def quoted_identifier(c)
178
+ "`#{c}`"
179
+ end
136
180
 
137
181
  def literal(v)
138
182
  case v
183
+ when LiteralString
184
+ v
185
+ when String
186
+ "'#{::SQLite3::Database.quote(v)}'"
139
187
  when Time
140
188
  literal(v.iso8601)
189
+ when Date, DateTime
190
+ literal(v.to_s)
141
191
  else
142
192
  super
143
193
  end
@@ -1,70 +1,118 @@
1
- require 'thread'
2
-
3
1
  # A ConnectionPool manages access to database connections by keeping
4
2
  # multiple connections and giving threads exclusive access to each
5
3
  # connection.
6
4
  class ConnectionPool
7
- attr_reader :mutex
5
+ # An array of connections currently being used
6
+ attr_reader :allocated
7
+
8
+ # An array of connections opened but not currently used
9
+ attr_reader :available_connections
10
+
11
+ # The proc used to create a new database connection.
12
+ attr_accessor :connection_proc
8
13
 
14
+ # The total number of connections opened, should
15
+ # be equal to available_connections.length +
16
+ # allocated.length
17
+ attr_reader :created_count
18
+ alias_method :size, :created_count
19
+
9
20
  # The maximum number of connections.
10
21
  attr_reader :max_size
11
22
 
12
- # The proc used to create a new connection.
13
- attr_accessor :connection_proc
23
+ # The mutex that protects access to the other internal vairables. You must use
24
+ # this if you want to manipulate the variables safely.
25
+ attr_reader :mutex
14
26
 
15
- attr_reader :available_connections, :allocated, :created_count
16
27
 
17
28
  # Constructs a new pool with a maximum size. If a block is supplied, it
18
29
  # is used to create new connections as they are needed.
19
30
  #
20
- # pool = ConnectionPool.new(10) {MyConnection.new(opts)}
31
+ # pool = ConnectionPool.new(:max_connections=>10) {MyConnection.new(opts)}
21
32
  #
22
33
  # The connection creation proc can be changed at any time by assigning a
23
34
  # Proc to pool#connection_proc.
24
35
  #
25
- # pool = ConnectionPool.new(10)
36
+ # pool = ConnectionPool.new(:max_connections=>10)
26
37
  # pool.connection_proc = proc {MyConnection.new(opts)}
27
- def initialize(max_size = 4, &block)
28
- @max_size = max_size
38
+ #
39
+ # The connection pool takes the following options:
40
+ #
41
+ # * :max_connections - The maximum number of connections the connection pool
42
+ # will open (default 4)
43
+ # * :pool_convert_exceptions - Whether to convert non-StandardError based exceptions
44
+ # to RuntimeError exceptions (default true)
45
+ # * :pool_reuse_connections - Which strategy to follow in regards to reusing connections:
46
+ # * :always - Always reuse a connection that belongs to the same thread
47
+ # * :allow - Only reuse a connection that belongs to the same thread if
48
+ # another cannot be acquired immediately (default)
49
+ # * :last_resort - Only reuse a connection that belongs to the same thread if
50
+ # the pool timeout has expired
51
+ # * :never - Never reuse a connection that belongs to the same thread
52
+ # * :pool_sleep_time - The amount of time to sleep before attempting to acquire
53
+ # a connection again (default 0.001)
54
+ # * :pool_timeout - The amount of seconds to wait to acquire a connection
55
+ # before raising a PoolTimeoutError (default 5)
56
+ def initialize(opts = {}, &block)
57
+ @max_size = opts[:max_connections] || 4
29
58
  @mutex = Mutex.new
30
59
  @connection_proc = block
31
60
 
32
61
  @available_connections = []
33
- @allocated = {}
62
+ @allocated = []
34
63
  @created_count = 0
64
+ @timeout = opts[:pool_timeout] || 5
65
+ @sleep_time = opts[:pool_sleep_time] || 0.001
66
+ @reuse_connections = opts[:pool_reuse_connections] || :allow
67
+ @convert_exceptions = opts.include?(:pool_convert_exceptions) ? opts[:pool_convert_exceptions] : true
35
68
  end
36
69
 
37
- # Returns the number of created connections.
38
- def size
39
- @created_count
40
- end
41
-
42
- # Assigns a connection to the current thread, yielding the connection
43
- # to the supplied block.
70
+ # Chooses the first available connection, or if none are available,
71
+ # creates a new connection. Passes the connection to the supplied block:
44
72
  #
45
73
  # pool.hold {|conn| conn.execute('DROP TABLE posts')}
46
74
  #
47
- # Pool#hold is re-entrant, meaning it can be called recursively in
48
- # the same thread without blocking.
75
+ # Pool#hold can be re-entrant, meaning it can be called recursively in
76
+ # the same thread without blocking if the :pool_reuse_connections option
77
+ # was set to :always or :allow. Depending on the :pool_reuse_connections
78
+ # option you may get the connection currently used by the thread or a new connection.
49
79
  #
50
- # If no connection is available, Pool#hold will block until a connection
51
- # is available.
80
+ # If no connection is immediately available and the pool is already using the maximum
81
+ # number of connections, Pool#hold will block until a connection
82
+ # is available or the timeout expires. If the timeout expires before a
83
+ # connection can be acquired, a Sequel::Error::PoolTimeoutError is
84
+ # raised.
52
85
  def hold
53
- t = Thread.current
54
- if (conn = owned_connection(t))
55
- return yield(conn)
56
- end
57
- while !(conn = acquire(t))
58
- sleep 0.001
59
- end
60
86
  begin
61
- yield conn
62
- ensure
63
- release(t)
87
+ t = Thread.current
88
+ time = Time.new
89
+ timeout = time + @timeout
90
+ sleep_time = @sleep_time
91
+ reuse = @reuse_connections
92
+ if (reuse == :always) && (conn = owned_connection(t))
93
+ return yield(conn)
94
+ end
95
+ reuse = reuse == :allow ? true : false
96
+ until conn = acquire(t)
97
+ if reuse && (conn = owned_connection(t))
98
+ return yield(conn)
99
+ end
100
+ if Time.new > timeout
101
+ if (@reuse_connections == :last_resort) && (conn = owned_connection(t))
102
+ return yield(conn)
103
+ end
104
+ raise(::Sequel::Error::PoolTimeoutError)
105
+ end
106
+ sleep sleep_time
107
+ end
108
+ begin
109
+ yield conn
110
+ ensure
111
+ release(t, conn)
112
+ end
113
+ rescue Exception => e
114
+ raise(@convert_exceptions && !e.is_a?(StandardError) ? RuntimeError.new(e.message) : e)
64
115
  end
65
- rescue Exception => e
66
- # if the error is not a StandardError it is converted into RuntimeError.
67
- raise e.is_a?(StandardError) ? e : e.message
68
116
  end
69
117
 
70
118
  # Removes all connection currently available, optionally yielding each
@@ -80,65 +128,84 @@ class ConnectionPool
80
128
  end
81
129
 
82
130
  private
83
- # Returns the connection owned by the supplied thread, if any.
84
- def owned_connection(thread)
85
- @mutex.synchronize {@allocated[thread]}
131
+
132
+ # Returns the connection owned by the supplied thread, if any.
133
+ def owned_connection(thread)
134
+ @mutex.synchronize do
135
+ x = @allocated.assoc(thread)
136
+ x[1] if x
86
137
  end
87
-
88
- # Assigns a connection to the supplied thread, if one is available.
89
- def acquire(thread)
90
- @mutex.synchronize do
91
- if conn = available
92
- @allocated[thread] = conn
93
- end
138
+ end
139
+
140
+ # Assigns a connection to the supplied thread, if one is available.
141
+ def acquire(thread)
142
+ @mutex.synchronize do
143
+ if conn = available
144
+ @allocated << [thread, conn]
145
+ conn
94
146
  end
95
147
  end
96
-
97
- # Returns an available connection. If no connection is available,
98
- # tries to create a new connection.
99
- def available
100
- @available_connections.pop || make_new
101
- end
102
-
103
- # Creates a new connection if the size of the pool is less than the
104
- # maximum size.
105
- def make_new
106
- if @created_count < @max_size
107
- @created_count += 1
108
- @connection_proc ? @connection_proc.call : \
109
- (raise Error, "No connection proc specified")
110
- end
148
+ end
149
+
150
+ # Returns an available connection. If no connection is available,
151
+ # tries to create a new connection.
152
+ def available
153
+ @available_connections.pop || make_new
154
+ end
155
+
156
+ # Creates a new connection if the size of the pool is less than the
157
+ # maximum size.
158
+ def make_new
159
+ if @created_count < @max_size
160
+ @created_count += 1
161
+ @connection_proc ? @connection_proc.call : \
162
+ (raise Error, "No connection proc specified")
111
163
  end
112
-
113
- # Releases the connection assigned to the supplied thread.
114
- def release(thread)
115
- @mutex.synchronize do
116
- @available_connections << @allocated[thread]
117
- @allocated.delete(thread)
118
- end
164
+ end
165
+
166
+ # Releases the connection assigned to the supplied thread.
167
+ def release(thread, conn)
168
+ @mutex.synchronize do
169
+ @allocated.delete([thread, conn])
170
+ @available_connections << conn
119
171
  end
172
+ end
120
173
  end
121
174
 
122
175
  # A SingleThreadedPool acts as a replacement for a ConnectionPool for use
123
176
  # in single-threaded applications. ConnectionPool imposes a substantial
124
177
  # performance penalty, so SingleThreadedPool is used to gain some speed.
178
+ #
179
+ # Note that using a single threaded pool with some adapters can cause
180
+ # errors in certain cases, see Sequel.single_threaded=.
125
181
  class SingleThreadedPool
182
+ # The single database connection for the pool
126
183
  attr_reader :conn
184
+
185
+ # The proc used to create a new database connection
127
186
  attr_writer :connection_proc
128
187
 
129
188
  # Initializes the instance with the supplied block as the connection_proc.
130
- def initialize(&block)
189
+ #
190
+ # The single threaded pool takes the following options:
191
+ #
192
+ # * :pool_convert_exceptions - Whether to convert non-StandardError based exceptions
193
+ # to RuntimeError exceptions (default true)
194
+ def initialize(opts={}, &block)
131
195
  @connection_proc = block
196
+ @convert_exceptions = opts.include?(:pool_convert_exceptions) ? opts[:pool_convert_exceptions] : true
132
197
  end
133
198
 
134
199
  # Yields the connection to the supplied block. This method simulates the
135
200
  # ConnectionPool#hold API.
136
201
  def hold
137
- @conn ||= @connection_proc.call
138
- yield @conn
139
- rescue Exception => e
140
- # if the error is not a StandardError it is converted into RuntimeError.
141
- raise e.is_a?(StandardError) ? e : e.message
202
+ begin
203
+ @conn ||= @connection_proc.call
204
+ yield @conn
205
+ rescue Exception => e
206
+ # if the error is not a StandardError it is converted into RuntimeError.
207
+ raise(@convert_exceptions && !e.is_a?(StandardError) ? RuntimeError.new(e.message) : e)
208
+ end
142
209
  end
143
210
 
144
211
  # Disconnects from the database. Once a connection is requested using
@@ -1,66 +1,201 @@
1
- # Enumerable extensions.
2
- module Enumerable
3
- # Invokes the specified method for each item, along with the supplied
4
- # arguments.
5
- def send_each(sym, *args)
6
- each {|i| i.send(sym, *args)}
7
- end
8
- end
9
-
10
- # Range extensions
11
- class Range
12
- # Returns the interval between the beginning and end of the range.
13
- def interval
14
- last - first
15
- end
16
- end
17
-
18
- # Object extensions
19
- class Object
20
- # Returns true if the object is a object of one of the classes
21
- def is_one_of?(*classes)
22
- classes.each {|c| return c if is_a?(c)}
23
- nil
24
- end
25
-
26
- # Objects are blank if they respond true to empty?
27
- def blank?
28
- nil? || (respond_to?(:empty?) && empty?)
29
- end
30
- end
31
-
32
- class Numeric
33
- # Numerics are never blank (not even 0)
34
- def blank?
35
- false
36
- end
37
- end
38
-
39
- class NilClass
40
- # nil is always blank
41
- def blank?
42
- true
43
- end
44
- end
45
-
46
- class TrueClass
47
- # true is never blank
48
- def blank?
49
- false
50
- end
51
- end
52
-
53
- class FalseClass
54
- # false is always blank
55
- def blank?
56
- true
57
- end
58
- end
59
-
60
- class String
61
- BLANK_STRING_REGEXP = /\A\s*\z/
62
- # Strings are blank if they are empty or include only whitespace
63
- def blank?
64
- empty? || BLANK_STRING_REGEXP.match(self)
65
- end
66
- end
1
+ # This file includes augmentations to the core ruby classes the Sequel uses,
2
+ # which are unrelated to the creation of SQL. It includes common
3
+ # idioms to reduce the amount of code duplication.
4
+
5
+ class Array
6
+ # True if the array is not empty and all of its elements are
7
+ # arrays of size 2. This is used to determine if the array
8
+ # could be a specifier of conditions, used similarly to a hash
9
+ # but allowing for duplicate keys.
10
+ #
11
+ # hash.to_a.all_two_pairs? # => true unless hash is empty
12
+ def all_two_pairs?
13
+ !empty? && all?{|i| (Array === i) && (i.length == 2)}
14
+ end
15
+
16
+ # Removes and returns the last member of the array if it is a hash. Otherwise,
17
+ # an empty hash is returned This method is useful when writing methods that
18
+ # take an options hash as the last parameter. For example:
19
+ #
20
+ # def validate_each(*args, &block)
21
+ # opts = args.extract_options!
22
+ # ...
23
+ # end
24
+ def extract_options!
25
+ last.is_a?(Hash) ? pop : {}
26
+ end
27
+ end
28
+
29
+ module Enumerable
30
+ # Invokes the specified method for each item, along with the supplied
31
+ # arguments.
32
+ def send_each(sym, *args)
33
+ each{|i| i.send(sym, *args)}
34
+ end
35
+ end
36
+
37
+ class FalseClass
38
+ # false is always blank
39
+ def blank?
40
+ true
41
+ end
42
+ end
43
+
44
+ # Add some metaprogramming methods to avoid class << self
45
+ class Module
46
+ # Defines an instance method within a class/module
47
+ def class_def(name, &block)
48
+ class_eval{define_method(name, &block)}
49
+ end
50
+
51
+ private
52
+
53
+ # Define instance method(s) that calls class method(s) of the
54
+ # same name. Replaces the construct:
55
+ #
56
+ # define_method(meth){self.class.send(meth)}
57
+ def class_attr_reader(*meths)
58
+ meths.each{|meth| define_method(meth){self.class.send(meth)}}
59
+ end
60
+
61
+ # Create an alias for a singleton/class method.
62
+ # Replaces the construct:
63
+ #
64
+ # class << self
65
+ # alias_method to, from
66
+ # end
67
+ def metaalias(to, from)
68
+ metaclass.instance_eval{alias_method to, from}
69
+ end
70
+
71
+ # Make a singleton/class attribute accessor method(s).
72
+ # Replaces the construct:
73
+ #
74
+ # class << self
75
+ # attr_accessor *meths
76
+ # end
77
+ def metaattr_accessor(*meths)
78
+ metaclass.instance_eval{attr_accessor(*meths)}
79
+ end
80
+
81
+ # Make a singleton/class method(s) private.
82
+ # Make a singleton/class attribute reader method(s).
83
+ # Replaces the construct:
84
+ #
85
+ # class << self
86
+ # attr_reader *meths
87
+ # end
88
+ def metaattr_reader(*meths)
89
+ metaclass.instance_eval{attr_reader(*meths)}
90
+ end
91
+
92
+ # Make a singleton/class method(s) private.
93
+ # Replaces the construct:
94
+ #
95
+ # class << self
96
+ # private *meths
97
+ # end
98
+ def metaprivate(*meths)
99
+ metaclass.instance_eval{private(*meths)}
100
+ end
101
+ end
102
+
103
+ # Helpers from Metaid and a bit more
104
+ class Object
105
+ # Objects are blank if they respond true to empty?
106
+ def blank?
107
+ respond_to?(:empty?) && empty?
108
+ end
109
+
110
+ # Returns true if the object is an instance of one of the classes
111
+ def is_one_of?(*classes)
112
+ !!classes.find{|c| is_a?(c)}
113
+ end
114
+
115
+ # Add methods to the object's metaclass
116
+ def meta_def(name, &block)
117
+ meta_eval{define_method(name, &block)}
118
+ end
119
+
120
+ # Evaluate the block in the context of the object's metaclass
121
+ def meta_eval(&block)
122
+ metaclass.instance_eval(&block)
123
+ end
124
+
125
+ # The hidden singleton lurks behind everyone
126
+ def metaclass
127
+ class << self
128
+ self
129
+ end
130
+ end
131
+ end
132
+
133
+ class NilClass
134
+ # nil is always blank
135
+ def blank?
136
+ true
137
+ end
138
+ end
139
+
140
+ class Numeric
141
+ # Numerics are never blank (not even 0)
142
+ def blank?
143
+ false
144
+ end
145
+ end
146
+
147
+ class Range
148
+ # Returns the interval between the beginning and end of the range.
149
+ #
150
+ # For exclusive ranges, is one less than the inclusive range:
151
+ #
152
+ # (0..10).interval # => 10
153
+ # (0...10).interval # => 9
154
+ #
155
+ # Only works for numeric ranges, for other ranges the result is undefined,
156
+ # and the method may raise an error.
157
+ def interval
158
+ last - first - (exclude_end? ? 1 : 0)
159
+ end
160
+ end
161
+
162
+ class String
163
+ # Strings are blank if they are empty or include only whitespace
164
+ def blank?
165
+ strip.empty?
166
+ end
167
+
168
+ # Converts a string into a Date object.
169
+ def to_date
170
+ begin
171
+ Date.parse(self)
172
+ rescue => e
173
+ raise Sequel::Error::InvalidValue, "Invalid date value '#{self}' (#{e.message})"
174
+ end
175
+ end
176
+
177
+ # Converts a string into a DateTime object.
178
+ def to_datetime
179
+ begin
180
+ DateTime.parse(self)
181
+ rescue => e
182
+ raise Sequel::Error::InvalidValue, "Invalid date value '#{self}' (#{e.message})"
183
+ end
184
+ end
185
+
186
+ # Converts a string into a Time object.
187
+ def to_time
188
+ begin
189
+ Time.parse(self)
190
+ rescue => e
191
+ raise Sequel::Error::InvalidValue, "Invalid time value '#{self}' (#{e.message})"
192
+ end
193
+ end
194
+ end
195
+
196
+ class TrueClass
197
+ # true is never blank
198
+ def blank?
199
+ false
200
+ end
201
+ end