beaneater 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.yardopts +8 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +399 -0
- data/REF +23 -0
- data/Rakefile +23 -0
- data/TODO +2 -0
- data/beaneater.gemspec +24 -0
- data/examples/demo.rb +96 -0
- data/lib/beaneater.rb +10 -0
- data/lib/beaneater/connection.rb +110 -0
- data/lib/beaneater/errors.rb +73 -0
- data/lib/beaneater/job.rb +2 -0
- data/lib/beaneater/job/collection.rb +91 -0
- data/lib/beaneater/job/record.rb +174 -0
- data/lib/beaneater/pool.rb +141 -0
- data/lib/beaneater/pool_command.rb +71 -0
- data/lib/beaneater/stats.rb +55 -0
- data/lib/beaneater/stats/fast_struct.rb +96 -0
- data/lib/beaneater/stats/stat_struct.rb +39 -0
- data/lib/beaneater/tube.rb +2 -0
- data/lib/beaneater/tube/collection.rb +134 -0
- data/lib/beaneater/tube/record.rb +158 -0
- data/lib/beaneater/version.rb +4 -0
- data/test/beaneater_test.rb +115 -0
- data/test/connection_test.rb +64 -0
- data/test/errors_test.rb +26 -0
- data/test/job_test.rb +213 -0
- data/test/jobs_test.rb +107 -0
- data/test/pool_command_test.rb +68 -0
- data/test/pool_test.rb +154 -0
- data/test/stat_struct_test.rb +41 -0
- data/test/stats_test.rb +42 -0
- data/test/test_helper.rb +21 -0
- data/test/tube_test.rb +164 -0
- data/test/tubes_test.rb +153 -0
- metadata +181 -0
@@ -0,0 +1,174 @@
|
|
1
|
+
module Beaneater
|
2
|
+
# Represents job related commands.
|
3
|
+
class Job
|
4
|
+
|
5
|
+
# @!attribute id
|
6
|
+
# @return [Integer] returns Job id
|
7
|
+
# @!attribute body
|
8
|
+
# @return [String] returns Job body
|
9
|
+
# @!attribute connection
|
10
|
+
# @return [Beaneater::Connection] returns Connection which has retrieved job
|
11
|
+
# @!attribute reserved
|
12
|
+
# @return [Boolean] returns If job is being reserved
|
13
|
+
attr_reader :id, :body, :connection, :reserved
|
14
|
+
|
15
|
+
|
16
|
+
# Initialize new connection
|
17
|
+
#
|
18
|
+
# @param [Hash] res result from beanstalkd response
|
19
|
+
def initialize(res)
|
20
|
+
@id = res[:id]
|
21
|
+
@body = res[:body]
|
22
|
+
@connection = res[:connection]
|
23
|
+
@reserved = res[:status] == 'RESERVED'
|
24
|
+
end
|
25
|
+
|
26
|
+
# Send command to bury job
|
27
|
+
#
|
28
|
+
# @param [Hash] options Settings to bury job
|
29
|
+
# @option options [Integer] pri Assign new priority to job
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# @beaneater_connection.bury({:pri => 100})
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
def bury(options={})
|
36
|
+
options = { :pri => stats.pri }.merge(options)
|
37
|
+
with_reserved("bury #{id} #{options[:pri]}") do
|
38
|
+
@reserved = false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Send command to release job
|
43
|
+
#
|
44
|
+
# @param [Hash] options Settings to release job
|
45
|
+
# @option options [Integer] pri Assign new priority to job
|
46
|
+
# @option options [Integer] pri Assign new delay to job
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
# @beaneater_connection.jobs.find(123).release(:pri => 10, :delay => 5)
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def release(options={})
|
53
|
+
options = { :pri => stats.pri, :delay => stats.delay }.merge(options)
|
54
|
+
with_reserved("release #{id} #{options[:pri]} #{options[:delay]}") do
|
55
|
+
@reserved = false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Send command to touch job
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# @beaneater_connection.jobs.find(123).touch
|
63
|
+
#
|
64
|
+
# @api public
|
65
|
+
def touch
|
66
|
+
with_reserved("touch #{id}")
|
67
|
+
end
|
68
|
+
|
69
|
+
# Send command to delete job
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# @beaneater_connection.jobs.find(123).delete
|
73
|
+
#
|
74
|
+
# @api public
|
75
|
+
def delete
|
76
|
+
transmit("delete #{id}") { @reserved = false }
|
77
|
+
end
|
78
|
+
|
79
|
+
# Send command to kick job
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# @beaneater_connection.jobs.find(123).kick
|
83
|
+
#
|
84
|
+
# @api public
|
85
|
+
def kick
|
86
|
+
transmit("kick-job #{id}")
|
87
|
+
end
|
88
|
+
|
89
|
+
# Send command to get stats about job
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# @beaneater_connection.jobs.find(123).stats
|
93
|
+
#
|
94
|
+
# @api public
|
95
|
+
def stats
|
96
|
+
res = transmit("stats-job #{id}")
|
97
|
+
StatStruct.from_hash(res[:body])
|
98
|
+
end
|
99
|
+
|
100
|
+
# Check if job is being reserved
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
# @beaneater_connection.jobs.find(123).reserved?
|
104
|
+
#
|
105
|
+
# @api public
|
106
|
+
def reserved?
|
107
|
+
@reserved || self.stats.state == "reserved"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Check if job exists
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# @beaneater_connection.jobs.find(123).exists?
|
114
|
+
#
|
115
|
+
# @api public
|
116
|
+
def exists?
|
117
|
+
!!self.stats
|
118
|
+
rescue Beaneater::NotFoundError
|
119
|
+
false
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns the name of the tube this job is in
|
123
|
+
#
|
124
|
+
# @example
|
125
|
+
# @beaneater_connection.jobs.find(123).tube
|
126
|
+
#
|
127
|
+
# @api public
|
128
|
+
def tube
|
129
|
+
self.stats && self.stats.tube
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns string representation of job
|
133
|
+
#
|
134
|
+
# @example
|
135
|
+
# @beaneater_connection.jobs.find(123).to_s
|
136
|
+
# @beaneater_connection.jobs.find(123).inspect
|
137
|
+
#
|
138
|
+
# @api public
|
139
|
+
def to_s
|
140
|
+
"#<Beaneater::Job id=#{id} body=#{body.inspect}>"
|
141
|
+
end
|
142
|
+
alias :inspect :to_s
|
143
|
+
|
144
|
+
protected
|
145
|
+
|
146
|
+
# Transmit command to beanstalkd instances and fetch response.
|
147
|
+
#
|
148
|
+
# @param [String] cmd Beanstalkd command to send.
|
149
|
+
# @return [Hash] Beanstalkd response for the command.
|
150
|
+
# @example
|
151
|
+
# transmit('stats')
|
152
|
+
# transmit('stats') { 'success' }
|
153
|
+
#
|
154
|
+
def transmit(cmd, &block)
|
155
|
+
res = connection.transmit(cmd)
|
156
|
+
yield if block_given?
|
157
|
+
res
|
158
|
+
end
|
159
|
+
|
160
|
+
# Transmits a command which requires the job to be reserved.
|
161
|
+
#
|
162
|
+
# @param [String] cmd Beanstalkd command to send.
|
163
|
+
# @return [Hash] Beanstalkd response for the command.
|
164
|
+
# @raise [Beaneater::JobNotReserved] Command cannot execute since job is not reserved.
|
165
|
+
# @example
|
166
|
+
# with_reserved("bury 26") { @reserved = false }
|
167
|
+
#
|
168
|
+
def with_reserved(cmd, &block)
|
169
|
+
raise JobNotReserved unless reserved?
|
170
|
+
transmit(cmd, &block)
|
171
|
+
end
|
172
|
+
|
173
|
+
end # Job
|
174
|
+
end # Beaneater
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# Simple ruby client for beanstalkd.
|
2
|
+
module Beaneater
|
3
|
+
# Represents collection of connections.
|
4
|
+
class Pool
|
5
|
+
# Default number of retries to send a command to a connection
|
6
|
+
MAX_RETRIES = 3
|
7
|
+
|
8
|
+
# @!attribute connections
|
9
|
+
# @return [Array<Beaneater::Connection>] returns Collection of connections
|
10
|
+
attr_reader :connections
|
11
|
+
|
12
|
+
# Initialize new connection
|
13
|
+
#
|
14
|
+
# @param [Array] hosts Array of beanstalkd server host
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# Beaneater::Pool.new(['localhost:11300', '127.0.0.1:11300'])
|
18
|
+
#
|
19
|
+
# ENV['BEANSTALKD_URL'] = 'localhost:11300,127.0.0.1:11300'
|
20
|
+
# @bp = Beaneater::Pool.new
|
21
|
+
# @bp.connections.first.host # => 'localhost'
|
22
|
+
# @bp.connections.last.host # => '127.0.0.1'
|
23
|
+
def initialize(hosts=nil)
|
24
|
+
hosts = hosts || host_from_env
|
25
|
+
@connections = Array(hosts).map { |h| Connection.new(h) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns Beaneater::Stats object
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def stats
|
32
|
+
@stats ||= Stats.new(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns Beaneater::Jobs object
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def jobs
|
39
|
+
@jobs ||= Jobs.new(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns Beaneater::Tubes object
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
def tubes
|
46
|
+
@tubes ||= Tubes.new(self)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Send command to every beanstalkd servers set in pool
|
50
|
+
#
|
51
|
+
# @param [String] command Beanstalkd command
|
52
|
+
# @param [Hash] options telnet connections options
|
53
|
+
# @param [Proc] block Block passed in telnet connection object
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# @pool.transmit_to_all("stats")
|
57
|
+
def transmit_to_all(command, options={}, &block)
|
58
|
+
connections.map do |conn|
|
59
|
+
safe_transmit { conn.transmit(command, options, &block) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Send command to a random beanstalkd servers set in pool
|
64
|
+
#
|
65
|
+
# @param [String] command Beanstalkd command
|
66
|
+
# @param [Hash] options telnet connections options
|
67
|
+
# @param [Proc] block Block passed in telnet connection object
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# @pool.transmit_to_rand("stats", :match => /\n/)
|
71
|
+
def transmit_to_rand(command, options={}, &block)
|
72
|
+
safe_transmit do
|
73
|
+
conn = connections.respond_to?(:sample) ? connections.sample : connections.choice
|
74
|
+
conn.transmit(command, options, &block)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Send command to each beanstalkd servers until getting response expected
|
79
|
+
#
|
80
|
+
# @param [String] command Beanstalkd command
|
81
|
+
# @param [Hash] options telnet connections options
|
82
|
+
# @param [Proc] block Block passed in telnet connection object
|
83
|
+
#
|
84
|
+
# @example
|
85
|
+
# @pool.transmit_until_res('peek-ready', :status => "FOUND", &block)
|
86
|
+
def transmit_until_res(command, options={}, &block)
|
87
|
+
status_expected = options.delete(:status)
|
88
|
+
connections.each do |conn|
|
89
|
+
res = safe_transmit { conn.transmit(command, options, &block) }
|
90
|
+
return res if res[:status] == status_expected
|
91
|
+
end && nil
|
92
|
+
end
|
93
|
+
|
94
|
+
# Closes all connections within pool
|
95
|
+
def close
|
96
|
+
while @connections.any?
|
97
|
+
conn = @connections.pop
|
98
|
+
conn.close
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
|
104
|
+
# Transmit command to beanstalk connections safely handling failed connections
|
105
|
+
#
|
106
|
+
# @param [Proc] block The command to execute.
|
107
|
+
# @return [Object] Result of the block passed
|
108
|
+
# @raise [Beaneater::DrainingError,Beaneater::NotConnected] Could not connect to Beanstalk client
|
109
|
+
# @example
|
110
|
+
# safe_transmit { conn.transmit('foo') }
|
111
|
+
# # => "result of foo command from beanstalk"
|
112
|
+
#
|
113
|
+
def safe_transmit(&block)
|
114
|
+
retries = 1
|
115
|
+
begin
|
116
|
+
yield
|
117
|
+
rescue DrainingError, EOFError, Errno::ECONNRESET, Errno::EPIPE => ex
|
118
|
+
# TODO remove faulty connections from pool?
|
119
|
+
# https://github.com/kr/beanstalk-client-ruby/blob/master/lib/beanstalk-client/connection.rb#L405-410
|
120
|
+
if retries < MAX_RETRIES
|
121
|
+
retries += 1
|
122
|
+
retry
|
123
|
+
else # finished retrying, fail out
|
124
|
+
ex.is_a?(DrainingError) ? raise(ex) : raise(NotConnected, "Could not connect!")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end # transmit_call
|
128
|
+
|
129
|
+
# The hosts provided by BEANSTALKD_URL environment variable, if available.
|
130
|
+
#
|
131
|
+
# @return [Array] Set of beanstalkd host addresses
|
132
|
+
# @example
|
133
|
+
# ENV['BEANSTALKD_URL'] = "localhost:1212,localhost:2424"
|
134
|
+
# # => ['localhost:1212', 'localhost:2424']
|
135
|
+
#
|
136
|
+
def host_from_env
|
137
|
+
ENV['BEANSTALKD_URL'].respond_to?(:length) && ENV['BEANSTALKD_URL'].length > 0 && ENV['BEANSTALKD_URL'].split(',').map(&:strip)
|
138
|
+
end
|
139
|
+
|
140
|
+
end # Pool
|
141
|
+
end # Beaneater
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Beaneater
|
4
|
+
# Represents collection of pool related commands.
|
5
|
+
class PoolCommand
|
6
|
+
# @!attribute pool
|
7
|
+
# @return [Beaneater::Pool] returns Pool object
|
8
|
+
attr_reader :pool
|
9
|
+
|
10
|
+
# Initialize new connection
|
11
|
+
#
|
12
|
+
# @param [Beaneater::Pool] pool Pool object
|
13
|
+
def initialize(pool)
|
14
|
+
@pool = pool
|
15
|
+
end
|
16
|
+
|
17
|
+
# Delegate to Pool#transmit_to_all and if needed will merge responses from beanstalkd
|
18
|
+
#
|
19
|
+
# @param [String] body Beanstalkd command
|
20
|
+
# @param [Hash] options telnet connections options
|
21
|
+
# @option options [Boolean] merge Ask for merging responses or not
|
22
|
+
# @param [Proc] block Block passed in telnet connection object
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# @pool.transmit_to_all("stats")
|
26
|
+
def transmit_to_all(body, options={}, &block)
|
27
|
+
merge = options.delete(:merge)
|
28
|
+
res = pool.transmit_to_all(body, options, &block)
|
29
|
+
if merge
|
30
|
+
res = { :status => res.first[:status], :body => sum_hashes(res.map { |r| r[:body] }) }
|
31
|
+
end
|
32
|
+
res
|
33
|
+
end
|
34
|
+
|
35
|
+
# Delegate missing methods to pool
|
36
|
+
# @api public
|
37
|
+
def method_missing(name, *args, &block)
|
38
|
+
if pool.respond_to?(name)
|
39
|
+
pool.send(name, *args, &block)
|
40
|
+
else # not a known pool command
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
# Selects hashes from collection and then merges the individual key values
|
48
|
+
#
|
49
|
+
# @param [Array<Hash>] hs Collection of hash responses returned from beanstalkd
|
50
|
+
# @return [Hash] Merged responses combining values from all the hash bodies
|
51
|
+
# @example
|
52
|
+
# self.sum_hashes([{ :foo => 1, :bar => 5 }, { :foo => 2, :bar => 3 }])
|
53
|
+
# => { :foo => 3, :bar => 8 }
|
54
|
+
#
|
55
|
+
def sum_hashes(hs)
|
56
|
+
hs.select { |h| h.is_a?(Hash) }.
|
57
|
+
inject({}) { |a,b| a.merge(b) { |k,o,n| combine_stats(k, o, n) } }
|
58
|
+
end
|
59
|
+
|
60
|
+
# Combine two values for given key
|
61
|
+
#
|
62
|
+
# @param [String] k key name within response hash
|
63
|
+
# @return [Set,Integer] combined value for stat
|
64
|
+
# @example
|
65
|
+
# self.combine_stats('total_connections', 4, 5) # => 9
|
66
|
+
#
|
67
|
+
def combine_stats(k, a, b)
|
68
|
+
['name', 'version', 'pid'].include?(k) ? Set[a] + Set[b] : a + b
|
69
|
+
end
|
70
|
+
end # PoolCommand
|
71
|
+
end # Beaneater
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'beaneater/stats/fast_struct'
|
2
|
+
require 'beaneater/stats/stat_struct'
|
3
|
+
|
4
|
+
module Beaneater
|
5
|
+
# Represents stats related to the beanstalkd pool.
|
6
|
+
class Stats < PoolCommand
|
7
|
+
# Returns keys for stats data
|
8
|
+
#
|
9
|
+
# @return [Array<String>] Set of keys for stats.
|
10
|
+
# @example
|
11
|
+
# @bp.stats.keys # => ["version", "total_connections"]
|
12
|
+
#
|
13
|
+
def keys
|
14
|
+
data.keys
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns value for specified key.
|
18
|
+
#
|
19
|
+
# @param [String,Symbol] key Name of key to retrieve
|
20
|
+
# @return [String,Integer] Value of specified key
|
21
|
+
# @example
|
22
|
+
# @bp.stats['total_connections'] # => 4
|
23
|
+
#
|
24
|
+
def [](key)
|
25
|
+
data[key]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Defines a cached method for looking up data for specified key
|
29
|
+
# Protects against infinite loops by checking stacktrace
|
30
|
+
# @api public
|
31
|
+
def method_missing(name, *args, &block)
|
32
|
+
if caller.first !~ /`(method_missing|data')/ && data.keys.include?(name.to_s)
|
33
|
+
self.class.class_eval <<-CODE, __FILE__, __LINE__
|
34
|
+
def #{name}; data[#{name.inspect}]; end
|
35
|
+
CODE
|
36
|
+
data[name.to_s]
|
37
|
+
else # no key matches or caught infinite loop
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
# Returns struct based on stats data merged from all connections.
|
45
|
+
#
|
46
|
+
# @return [Beaneater::StatStruct] the combined stats for all beanstalk connections in the pool
|
47
|
+
# @example
|
48
|
+
# self.data # => { 'version' : 1.7, 'total_connections' : 23 }
|
49
|
+
# self.data.total_connections # => 23
|
50
|
+
#
|
51
|
+
def data
|
52
|
+
StatStruct.from_hash(transmit_to_all('stats', :merge => true)[:body])
|
53
|
+
end
|
54
|
+
end # Stats
|
55
|
+
end # Beaneater
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Beaneater
|
2
|
+
#
|
3
|
+
# Borrowed from:
|
4
|
+
# https://github.com/dolzenko/faster_open_struct/blob/master/lib/faster_open_struct.rb
|
5
|
+
#
|
6
|
+
# Up to 40 (!) times more memory efficient version of OpenStruct
|
7
|
+
#
|
8
|
+
# Differences from Ruby MRI OpenStruct:
|
9
|
+
#
|
10
|
+
# 1. Doesn't `dup` passed initialization hash (NOTE: only reference to hash is stored)
|
11
|
+
#
|
12
|
+
# 2. Doesn't convert hash keys to symbols (by default string keys are used,
|
13
|
+
# with fallback to symbol keys)
|
14
|
+
#
|
15
|
+
# 3. Creates methods on the fly on `OpenStruct` class, instead of singleton class.
|
16
|
+
# Uses `module_eval` with string to avoid holding scope references for every method.
|
17
|
+
#
|
18
|
+
# 4. Refactored, crud clean, spec covered :)
|
19
|
+
#
|
20
|
+
# @private
|
21
|
+
class FasterOpenStruct
|
22
|
+
# Undefine particularly nasty interfering methods on Ruby 1.8
|
23
|
+
undef :type if method_defined?(:type)
|
24
|
+
undef :id if method_defined?(:id)
|
25
|
+
|
26
|
+
def initialize(hash = nil)
|
27
|
+
@hash = hash || {}
|
28
|
+
@initialized_empty = hash == nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def method_missing(method_name_sym, *args)
|
32
|
+
if method_name_sym.to_s[-1] == ?=
|
33
|
+
if args.size != 1
|
34
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 1)", caller(1)
|
35
|
+
end
|
36
|
+
|
37
|
+
if self.frozen?
|
38
|
+
raise TypeError, "can't modify frozen #{self.class}", caller(1)
|
39
|
+
end
|
40
|
+
|
41
|
+
__new_ostruct_member__(method_name_sym.to_s.chomp("="))
|
42
|
+
send(method_name_sym, args[0])
|
43
|
+
elsif args.size == 0
|
44
|
+
__new_ostruct_member__(method_name_sym)
|
45
|
+
send(method_name_sym)
|
46
|
+
else
|
47
|
+
raise NoMethodError, "undefined method `#{method_name_sym}' for #{self}", caller(1)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def __new_ostruct_member__(method_name_sym)
|
52
|
+
self.class.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
|
53
|
+
def #{ method_name_sym }
|
54
|
+
@hash.fetch("#{ method_name_sym }", @hash[:#{ method_name_sym }]) # read by default from string key, then try symbol
|
55
|
+
# if string key doesn't exist
|
56
|
+
end
|
57
|
+
END_EVAL
|
58
|
+
|
59
|
+
unless method_name_sym.to_s[-1] == ?? # can't define writer for predicate method
|
60
|
+
self.class.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
|
61
|
+
def #{ method_name_sym }=(val)
|
62
|
+
if @hash.key?("#{ method_name_sym }") || @initialized_empty # write by default to string key (when it is present
|
63
|
+
# in initialization hash or initialization hash
|
64
|
+
# wasn't provided)
|
65
|
+
@hash["#{ method_name_sym }"] = val # if it doesn't exist - write to symbol key
|
66
|
+
else
|
67
|
+
@hash[:#{ method_name_sym }] = val
|
68
|
+
end
|
69
|
+
end
|
70
|
+
END_EVAL
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def empty?
|
75
|
+
@hash.empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Compare this object and +other+ for equality.
|
80
|
+
#
|
81
|
+
def ==(other)
|
82
|
+
return false unless other.is_a?(self.class)
|
83
|
+
@hash == other.instance_variable_get(:@hash)
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Returns a string containing a detailed summary of the keys and values.
|
88
|
+
#
|
89
|
+
def inspect
|
90
|
+
str = "#<#{ self.class }"
|
91
|
+
str << " #{ @hash.map { |k, v| "#{ k }=#{ v.inspect }" }.join(", ") }" unless @hash.empty?
|
92
|
+
str << ">"
|
93
|
+
end
|
94
|
+
alias :to_s :inspect
|
95
|
+
end # FasterOpenStruct
|
96
|
+
end # Beaneater
|