packet 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/LICENSE +4 -0
- data/README +251 -0
- data/Rakefile +87 -0
- data/TODO +0 -0
- data/bin/packet_mongrel.rb +215 -0
- data/bin/runner.rb +35 -0
- data/lib/attribute_accessors.rb +48 -0
- data/lib/bin_parser.rb +61 -0
- data/lib/buftok.rb +127 -0
- data/lib/callback.rb +14 -0
- data/lib/connection.rb +33 -0
- data/lib/core.rb +241 -0
- data/lib/cpu_worker.rb +19 -0
- data/lib/deferrable.rb +210 -0
- data/lib/disconnect_error.rb +8 -0
- data/lib/double_keyed_hash.rb +19 -0
- data/lib/event.rb +25 -0
- data/lib/io_worker.rb +6 -0
- data/lib/meta_pimp.rb +66 -0
- data/lib/nbio.rb +81 -0
- data/lib/packet.rb +37 -0
- data/lib/packet_guid.rb +16 -0
- data/lib/packet_master.rb +148 -0
- data/lib/packet_mongrel.rb +214 -0
- data/lib/periodic_event.rb +27 -0
- data/lib/pimp.rb +31 -0
- data/lib/ruby_hacks.rb +125 -0
- data/lib/worker.rb +120 -0
- metadata +75 -0
data/lib/packet_guid.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Packet
|
2
|
+
class Guid
|
3
|
+
def self.hexdigest
|
4
|
+
values = [
|
5
|
+
rand(0x0010000),
|
6
|
+
rand(0x0010000),
|
7
|
+
rand(0x0010000),
|
8
|
+
rand(0x0010000),
|
9
|
+
rand(0x0010000),
|
10
|
+
rand(0x1000000),
|
11
|
+
rand(0x1000000),
|
12
|
+
]
|
13
|
+
"%04x%04x%04x%04x%04x%06x%06x" % values
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# FIXME: Some code is duplicated between worker class and this Reactor class, that can be fixed
|
2
|
+
# with help of creation of Connection class and enabling automatic inheritance of that class and
|
3
|
+
# mixing in of methods from that class.
|
4
|
+
module Packet
|
5
|
+
class Reactor
|
6
|
+
include Core
|
7
|
+
attr_accessor :fd_writers, :msg_writers,:msg_reader
|
8
|
+
attr_accessor :live_workers
|
9
|
+
after_connection :provide_workers
|
10
|
+
|
11
|
+
def self.run
|
12
|
+
master_reactor_instance = new
|
13
|
+
master_reactor_instance.live_workers = DoubleKeyedHash.new
|
14
|
+
yield(master_reactor_instance)
|
15
|
+
master_reactor_instance.load_workers
|
16
|
+
master_reactor_instance.start_reactor
|
17
|
+
end # end of run method
|
18
|
+
|
19
|
+
def provide_workers(handler_instance,t_sock)
|
20
|
+
class << handler_instance
|
21
|
+
extend Forwardable
|
22
|
+
attr_accessor :workers,:connection,:reactor, :initialized,:signature
|
23
|
+
include NbioHelper
|
24
|
+
def send_data p_data
|
25
|
+
begin
|
26
|
+
write_data(p_data,connection)
|
27
|
+
rescue Errno::EPIPE
|
28
|
+
# probably a callback, when there is a error in writing to the socket
|
29
|
+
end
|
30
|
+
end
|
31
|
+
def invoke_init
|
32
|
+
@initialized = true
|
33
|
+
post_init
|
34
|
+
end
|
35
|
+
|
36
|
+
def close_connection
|
37
|
+
unbind
|
38
|
+
reactor.remove_connection(connection)
|
39
|
+
end
|
40
|
+
|
41
|
+
def close_connection_after_writing
|
42
|
+
connection.flush
|
43
|
+
unbind
|
44
|
+
reactor.remove_connection(connection)
|
45
|
+
end
|
46
|
+
|
47
|
+
def ask_worker(*args)
|
48
|
+
worker_name = args.shift
|
49
|
+
data_options = *args
|
50
|
+
data_options[:client_signature] = connection.fileno
|
51
|
+
workers[worker_name].send_request(data_options)
|
52
|
+
end
|
53
|
+
|
54
|
+
def send_object p_object
|
55
|
+
dump_object(p_object,connection)
|
56
|
+
end
|
57
|
+
def_delegators :@reactor, :start_server, :connect, :add_periodic_timer, :add_timer, :cancel_timer,:reconnect, :start_worker
|
58
|
+
end
|
59
|
+
handler_instance.workers = @live_workers
|
60
|
+
handler_instance.connection = t_sock
|
61
|
+
handler_instance.reactor = self
|
62
|
+
end
|
63
|
+
|
64
|
+
# FIXME: right now, each worker is tied to its connection and this can be problematic
|
65
|
+
# what if a worker wants to return results in a async manner
|
66
|
+
def handle_internal_messages(t_sock)
|
67
|
+
sock_fd = t_sock.fileno
|
68
|
+
worker_instance = @live_workers[sock_fd]
|
69
|
+
begin
|
70
|
+
raw_data = read_data(t_sock)
|
71
|
+
# t_data = Marshal.load(raw_data)
|
72
|
+
worker_instance.receive_data(raw_data) if worker_instance.respond_to?(:receive_data)
|
73
|
+
rescue DisconnectError => sock_error
|
74
|
+
read_ios.delete(t_sock)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# method loads workers in new processes
|
79
|
+
# FIXME: this method can be fixed, so as worker code can be actually, required
|
80
|
+
# only in forked process and hence saving upon the memory involved
|
81
|
+
# where worker is actually required in master as well as in worker.
|
82
|
+
def load_workers
|
83
|
+
|
84
|
+
if defined?(WORKER_ROOT)
|
85
|
+
worker_root = WORKER_ROOT
|
86
|
+
else
|
87
|
+
worker_root = "#{PACKET_APP}/worker"
|
88
|
+
end
|
89
|
+
t_workers = Dir["#{worker_root}/**/*.rb"]
|
90
|
+
return if t_workers.blank?
|
91
|
+
t_workers.each do |b_worker|
|
92
|
+
worker_name = File.basename(b_worker,".rb")
|
93
|
+
require worker_name
|
94
|
+
worker_klass = Object.const_get(worker_name.classify)
|
95
|
+
next if worker_klass.no_auto_load
|
96
|
+
fork_and_load(worker_klass)
|
97
|
+
end
|
98
|
+
|
99
|
+
# FIXME: easiest and yet perhaps a bit ugly, its just to make sure that from each
|
100
|
+
# worker proxy one can access other workers
|
101
|
+
@live_workers.each do |key,worker_instance|
|
102
|
+
worker_instance.workers = @live_workers
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def start_worker(worker_name,options = {})
|
107
|
+
require worker_name.to_s
|
108
|
+
worker_klass = Object.const_get(worker_name.classify)
|
109
|
+
fork_and_load(worker_klass,options)
|
110
|
+
end
|
111
|
+
|
112
|
+
# method forks given worker file in a new process
|
113
|
+
def fork_and_load(worker_klass,worker_options = { })
|
114
|
+
t_worker_name = worker_klass.worker_name
|
115
|
+
worker_pimp = worker_klass.worker_proxy.to_s
|
116
|
+
|
117
|
+
# socket from which master process is going to read
|
118
|
+
master_read_end,worker_write_end = UNIXSocket.pair(Socket::SOCK_STREAM)
|
119
|
+
# socket to which master process is going to write
|
120
|
+
worker_read_end,master_write_end = UNIXSocket.pair(Socket::SOCK_STREAM)
|
121
|
+
worker_read_fd,master_write_fd = UNIXSocket.pair
|
122
|
+
|
123
|
+
if((pid = fork()).nil?)
|
124
|
+
$0 = "ruby #{worker_klass.worker_name}"
|
125
|
+
master_write_end.close
|
126
|
+
master_read_end.close
|
127
|
+
master_write_fd.close
|
128
|
+
# master_write_end.close if master_write_end
|
129
|
+
worker_klass.start_worker(:write_end => worker_write_end,:read_end => worker_read_end,:read_fd => worker_read_fd,:options => worker_options)
|
130
|
+
end
|
131
|
+
Process.detach(pid)
|
132
|
+
|
133
|
+
unless worker_pimp.blank?
|
134
|
+
require worker_pimp
|
135
|
+
pimp_klass = Object.const_get(worker_pimp.classify)
|
136
|
+
@live_workers[t_worker_name,master_read_end.fileno] = pimp_klass.new(master_write_end,pid,self)
|
137
|
+
else
|
138
|
+
@live_workers[t_worker_name,master_read_end.fileno] = Packet::MetaPimp.new(master_write_end,pid,self)
|
139
|
+
end
|
140
|
+
|
141
|
+
worker_read_end.close
|
142
|
+
worker_write_end.close
|
143
|
+
worker_read_fd.close
|
144
|
+
read_ios << master_read_end
|
145
|
+
end # end of fork_and_load method
|
146
|
+
|
147
|
+
end # end of Reactor class
|
148
|
+
end # end of Packet module
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# This module rewrites pieces of the very good Mongrel web server in
|
2
|
+
# order to change it from a threaded application to an event based
|
3
|
+
# application running inside an Packet event loop. It should
|
4
|
+
# be compatible with the existing Mongrel handlers for Rails,
|
5
|
+
# Camping, Nitro, etc....
|
6
|
+
|
7
|
+
require 'packet'
|
8
|
+
require 'mongrel'
|
9
|
+
|
10
|
+
module Mongrel
|
11
|
+
class MongrelProtocol < Packet::Connection
|
12
|
+
def post_init
|
13
|
+
@parser = HttpParser.new
|
14
|
+
@params = HttpParams.new
|
15
|
+
@nparsed = 0
|
16
|
+
@request = nil
|
17
|
+
@request_len = nil
|
18
|
+
@linebuffer = ''
|
19
|
+
end
|
20
|
+
|
21
|
+
def receive_data data
|
22
|
+
@linebuffer << data
|
23
|
+
@nparsed = @parser.execute(@params, @linebuffer, @nparsed) unless @parser.finished?
|
24
|
+
if @parser.finished?
|
25
|
+
if @request_len.nil?
|
26
|
+
@request_len = @params[::Mongrel::Const::CONTENT_LENGTH].to_i
|
27
|
+
script_name, path_info, handlers = ::Mongrel::HttpServer::Instance.classifier.resolve(@params[::Mongrel::Const::REQUEST_PATH])
|
28
|
+
if handlers
|
29
|
+
@params[::Mongrel::Const::PATH_INFO] = path_info
|
30
|
+
@params[::Mongrel::Const::SCRIPT_NAME] = script_name
|
31
|
+
@params[::Mongrel::Const::REMOTE_ADDR] = @params[::Mongrel::Const::HTTP_X_FORWARDED_FOR] #|| ::Socket.unpack_sockaddr_in(get_peername)[1]
|
32
|
+
@notifiers = handlers.select { |h| h.request_notify }
|
33
|
+
end
|
34
|
+
if @request_len > ::Mongrel::Const::MAX_BODY
|
35
|
+
new_buffer = Tempfile.new(::Mongrel::Const::MONGREL_TMP_BASE)
|
36
|
+
new_buffer.binmode
|
37
|
+
new_buffer << @linebuffer[@nparsed..-1]
|
38
|
+
@linebuffer = new_buffer
|
39
|
+
else
|
40
|
+
@linebuffer = StringIO.new(@linebuffer[@nparsed..-1])
|
41
|
+
@linebuffer.pos = @linebuffer.length
|
42
|
+
end
|
43
|
+
end
|
44
|
+
if @linebuffer.length >= @request_len
|
45
|
+
@linebuffer.rewind
|
46
|
+
::Mongrel::HttpServer::Instance.process_http_request(@params,@linebuffer,self)
|
47
|
+
@linebuffer.delete if Tempfile === @linebuffer
|
48
|
+
end
|
49
|
+
elsif @linebuffer.length > ::Mongrel::Const::MAX_HEADER
|
50
|
+
close_connection
|
51
|
+
raise ::Mongrel::HttpParserError.new("HEADER is longer than allowed, aborting client early.")
|
52
|
+
end
|
53
|
+
rescue ::Mongrel::HttpParserError
|
54
|
+
if $mongrel_debug_client
|
55
|
+
STDERR.puts "#{Time.now}: BAD CLIENT (#{params[Const::HTTP_X_FORWARDED_FOR] || client.peeraddr.last}): #$!"
|
56
|
+
STDERR.puts "#{Time.now}: REQUEST DATA: #{data.inspect}\n---\nPARAMS: #{params.inspect}\n---\n"
|
57
|
+
end
|
58
|
+
close_connection
|
59
|
+
rescue Exception => e
|
60
|
+
close_connection
|
61
|
+
raise e
|
62
|
+
end
|
63
|
+
|
64
|
+
def write data
|
65
|
+
send_data data
|
66
|
+
end
|
67
|
+
|
68
|
+
def closed?
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
class HttpServer
|
75
|
+
def initialize(host, port, num_processors=950, x=0, y=nil) # Deal with Mongrel 1.0.1 or earlier, as well as later.
|
76
|
+
@socket = nil
|
77
|
+
@classifier = URIClassifier.new
|
78
|
+
@host = host
|
79
|
+
@port = port
|
80
|
+
@workers = ThreadGroup.new
|
81
|
+
if y
|
82
|
+
@throttle = x
|
83
|
+
@timeout = y || 60
|
84
|
+
else
|
85
|
+
@timeout = x
|
86
|
+
end
|
87
|
+
@num_processors = num_processors #num_processors is pointless for evented....
|
88
|
+
@death_time = 60
|
89
|
+
self.class.const_set(:Instance,self)
|
90
|
+
end
|
91
|
+
|
92
|
+
def run
|
93
|
+
trap('INT') { raise StopServer }
|
94
|
+
trap('TERM') { raise StopServer }
|
95
|
+
@acceptor = Thread.new do
|
96
|
+
Packet::Reactor.run do |t_reactor|
|
97
|
+
begin
|
98
|
+
t_reactor.start_server(@host,@port,MongrelProtocol)
|
99
|
+
rescue StopServer
|
100
|
+
t_reactor.start_server
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def process_http_request(params,linebuffer,client)
|
107
|
+
if not params[Const::REQUEST_PATH]
|
108
|
+
uri = URI.parse(params[Const::REQUEST_URI])
|
109
|
+
params[Const::REQUEST_PATH] = uri.request_uri
|
110
|
+
end
|
111
|
+
|
112
|
+
raise "No REQUEST PATH" if not params[Const::REQUEST_PATH]
|
113
|
+
|
114
|
+
script_name, path_info, handlers = @classifier.resolve(params[Const::REQUEST_PATH])
|
115
|
+
|
116
|
+
if handlers
|
117
|
+
notifiers = handlers.select { |h| h.request_notify }
|
118
|
+
request = HttpRequest.new(params, linebuffer, notifiers)
|
119
|
+
|
120
|
+
# request is good so far, continue processing the response
|
121
|
+
response = HttpResponse.new(client)
|
122
|
+
|
123
|
+
# Process each handler in registered order until we run out or one finalizes the response.
|
124
|
+
dispatch_to_handlers(handlers,request,response)
|
125
|
+
|
126
|
+
# And finally, if nobody closed the response off, we finalize it.
|
127
|
+
unless response.done
|
128
|
+
response.finished
|
129
|
+
else
|
130
|
+
response.close_connection
|
131
|
+
end
|
132
|
+
else
|
133
|
+
# Didn't find it, return a stock 404 response.
|
134
|
+
client.send_data(Const::ERROR_404_RESPONSE)
|
135
|
+
client.close_connection
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def dispatch_to_handlers(handlers,request,response)
|
140
|
+
handlers.each do |handler|
|
141
|
+
handler.process(request, response)
|
142
|
+
break if response.done
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class HttpRequest
|
148
|
+
def initialize(params, linebuffer, dispatchers)
|
149
|
+
@params = params
|
150
|
+
@dispatchers = dispatchers
|
151
|
+
@body = linebuffer
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class HttpResponse
|
156
|
+
def send_file(path, small_file = false)
|
157
|
+
File.open(path, "rb") do |f|
|
158
|
+
while chunk = f.read(Const::CHUNK_SIZE) and chunk.length > 0
|
159
|
+
begin
|
160
|
+
write(chunk)
|
161
|
+
rescue Object => exc
|
162
|
+
break
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
@body_sent = true
|
167
|
+
end
|
168
|
+
|
169
|
+
def write(data)
|
170
|
+
@socket.send_data data
|
171
|
+
end
|
172
|
+
|
173
|
+
def close_connection_after_writing
|
174
|
+
@socket.close_connection
|
175
|
+
end
|
176
|
+
|
177
|
+
def socket_error(details)
|
178
|
+
@socket.close_connection
|
179
|
+
done = true
|
180
|
+
raise details
|
181
|
+
end
|
182
|
+
|
183
|
+
def finished
|
184
|
+
send_status
|
185
|
+
send_header
|
186
|
+
send_body
|
187
|
+
@socket.close_connection
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
class Configurator
|
192
|
+
# This version fixes a bug in the regular Mongrel version by adding
|
193
|
+
# initialization of groups.
|
194
|
+
def change_privilege(user, group)
|
195
|
+
if user and group
|
196
|
+
log "Initializing groups for {#user}:{#group}."
|
197
|
+
Process.initgroups(user,Etc.getgrnam(group).gid)
|
198
|
+
end
|
199
|
+
|
200
|
+
if group
|
201
|
+
log "Changing group to #{group}."
|
202
|
+
Process::GID.change_privilege(Etc.getgrnam(group).gid)
|
203
|
+
end
|
204
|
+
|
205
|
+
if user
|
206
|
+
log "Changing user to #{user}."
|
207
|
+
Process::UID.change_privilege(Etc.getpwnam(user).uid)
|
208
|
+
end
|
209
|
+
rescue Errno::EPERM
|
210
|
+
log "FAILED to change user:group #{user}:#{group}: #$!"
|
211
|
+
exit 1
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Packet
|
2
|
+
class PeriodicEvent
|
3
|
+
attr_accessor :block, :timer_signature, :interval, :cancel_flag
|
4
|
+
def initialize(interval, &block)
|
5
|
+
@cancel_flag = false
|
6
|
+
@timer_signature = Guid.hexdigest
|
7
|
+
@block = block
|
8
|
+
@scheduled_time = Time.now + interval
|
9
|
+
@interval = interval
|
10
|
+
end
|
11
|
+
|
12
|
+
def run_now?
|
13
|
+
return true if @scheduled_time <= Time.now
|
14
|
+
return false
|
15
|
+
end
|
16
|
+
|
17
|
+
def cancel
|
18
|
+
@cancel_flag = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
@scheduled_time += @interval
|
23
|
+
@block.call
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
data/lib/pimp.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Packet
|
2
|
+
class Pimp
|
3
|
+
include NbioHelper
|
4
|
+
extend Forwardable
|
5
|
+
iattr_accessor :pimp_name
|
6
|
+
attr_accessor :lifeline, :pid, :signature
|
7
|
+
attr_accessor :fd_write_end
|
8
|
+
attr_accessor :workers, :reactor
|
9
|
+
|
10
|
+
def initialize(lifeline_socket,worker_pid,p_reactor)
|
11
|
+
@lifeline = lifeline_socket
|
12
|
+
@pid = worker_pid
|
13
|
+
@reactor = p_reactor
|
14
|
+
@signature = Guid.hexdigest
|
15
|
+
pimp_init if self.respond_to?(:pimp_init)
|
16
|
+
end
|
17
|
+
|
18
|
+
# encode the data, before writing to the socket
|
19
|
+
def send_data p_data
|
20
|
+
dump_object(p_data,@lifeline)
|
21
|
+
end
|
22
|
+
|
23
|
+
def send_fd sock_fd
|
24
|
+
@fd_write_end.send_io(sock_fd)
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_method :do_work, :send_data
|
28
|
+
def_delegators :@reactor, :connections
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
data/lib/ruby_hacks.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# FIXME: ultimate goal should be remove dependence from activesupport gem altogether.
|
2
|
+
|
3
|
+
class Object
|
4
|
+
def deep_copy
|
5
|
+
Marshal.load(Marshal.dump(self))
|
6
|
+
end
|
7
|
+
|
8
|
+
def nothing?
|
9
|
+
if respond_to?(:empty?) && respond_to?(:strip)
|
10
|
+
empty? or strip.empty?
|
11
|
+
elsif respond_to?(:empty?)
|
12
|
+
empty?
|
13
|
+
elsif respond_to?(:zero?)
|
14
|
+
zero?
|
15
|
+
else
|
16
|
+
!self
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def clean!
|
21
|
+
if respond_to?(:empty?) && respond_to?(:strip)
|
22
|
+
return nil if empty?
|
23
|
+
(strip.empty?) ? nil : (self.strip)
|
24
|
+
elsif respond_to?(:empty?)
|
25
|
+
empty? ? nil : self
|
26
|
+
else
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def blank?
|
32
|
+
if respond_to?(:empty?) && respond_to?(:strip)
|
33
|
+
empty? or strip.empty?
|
34
|
+
elsif respond_to?(:empty?)
|
35
|
+
empty?
|
36
|
+
else
|
37
|
+
!self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.metaclass; class << self; self; end; end
|
42
|
+
|
43
|
+
def self.iattr_accessor *args
|
44
|
+
|
45
|
+
metaclass.instance_eval do
|
46
|
+
attr_accessor *args
|
47
|
+
args.each do |attr|
|
48
|
+
define_method("set_#{attr}") do |b_value|
|
49
|
+
self.send("#{attr}=",b_value)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
args.each do |attr|
|
55
|
+
class_eval do
|
56
|
+
define_method(attr) do
|
57
|
+
self.class.send(attr)
|
58
|
+
end
|
59
|
+
define_method("#{attr}=") do |b_value|
|
60
|
+
self.class.send("#{attr}=",b_value)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
class NilClass #:nodoc:
|
69
|
+
def blank?
|
70
|
+
true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class FalseClass #:nodoc:
|
75
|
+
def blank?
|
76
|
+
true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class TrueClass #:nodoc:
|
81
|
+
def blank?
|
82
|
+
false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class Array #:nodoc:
|
87
|
+
alias_method :blank?, :empty?
|
88
|
+
def clean!
|
89
|
+
(empty?) ? nil : self
|
90
|
+
end
|
91
|
+
|
92
|
+
def extract_options!
|
93
|
+
last.is_a?(Hash) ? pop : {}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Hash #:nodoc:
|
98
|
+
alias_method :blank?, :empty?
|
99
|
+
def clean!
|
100
|
+
(empty?) ? nil : self
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class String #:nodoc:
|
105
|
+
def blank?
|
106
|
+
empty? || strip.empty?
|
107
|
+
end
|
108
|
+
|
109
|
+
def clean!
|
110
|
+
return nil if empty?
|
111
|
+
t_val = self.strip
|
112
|
+
(t_val.empty?) ? nil : t_val
|
113
|
+
end
|
114
|
+
|
115
|
+
def classify
|
116
|
+
word_parts = split('_')
|
117
|
+
return word_parts.map { |x| x.capitalize}.join
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Numeric #:nodoc:
|
122
|
+
def blank?
|
123
|
+
false
|
124
|
+
end
|
125
|
+
end
|