packet 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|