beaneater 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/.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
|