spacebunny 1.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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +190 -0
- data/Rakefile +6 -0
- data/assets/logo.png +0 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/examples/device/auto_config_publish.rb +87 -0
- data/examples/device/manual_config.rb +83 -0
- data/examples/device/publish_with_confirm.rb +85 -0
- data/examples/device/receive_messages.rb +109 -0
- data/examples/device/tls_connection.rb +65 -0
- data/examples/live_stream/receive_messages.rb +128 -0
- data/examples/live_stream/tls_connection.rb +22 -0
- data/lib/spacebunny.rb +23 -0
- data/lib/spacebunny/device/amqp.rb +158 -0
- data/lib/spacebunny/device/base.rb +229 -0
- data/lib/spacebunny/device/message.rb +56 -0
- data/lib/spacebunny/endpoint_connection.rb +128 -0
- data/lib/spacebunny/exceptions.rb +143 -0
- data/lib/spacebunny/live_stream/amqp.rb +124 -0
- data/lib/spacebunny/live_stream/base.rb +198 -0
- data/lib/spacebunny/live_stream/message.rb +38 -0
- data/lib/spacebunny/logger.rb +35 -0
- data/lib/spacebunny/utils.rb +185 -0
- data/lib/spacebunny/version.rb +3 -0
- data/spacebunny.gemspec +27 -0
- metadata +157 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
|
3
|
+
module Spacebunny
|
4
|
+
module LiveStream
|
5
|
+
class Amqp < Base
|
6
|
+
DEFAULT_QUEUE_OPTIONS = { passive: true }
|
7
|
+
DEFAULT_EXCHANGE_OPTIONS = { passive: true }
|
8
|
+
ACK_TYPES = [:manual, :auto]
|
9
|
+
|
10
|
+
attr_reader :built_live_streams, :client
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
super(:amqp, *args)
|
14
|
+
@built_live_streams = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def connect
|
18
|
+
# 'Fix' attributes: start from common connection configs and adjust attributes to match what Bunny
|
19
|
+
# wants as connection args
|
20
|
+
connection_params = connection_configs.dup
|
21
|
+
connection_params[:user] = connection_params.delete :client
|
22
|
+
connection_params[:password] = connection_params.delete :secret
|
23
|
+
connection_params[:port] = connection_params.delete(:tls_port) if connection_params[:tls]
|
24
|
+
connection_params[:recover_from_connection_close] = connection_params.delete :auto_recover
|
25
|
+
connection_params[:log_level] = connection_params.delete(:log_level) || ::Logger::ERROR
|
26
|
+
|
27
|
+
# Re-create client every time connect is called
|
28
|
+
@client = Bunny.new(connection_params)
|
29
|
+
@client.start
|
30
|
+
end
|
31
|
+
|
32
|
+
def channel_from_name(name)
|
33
|
+
# In @built_channels in fact we have exchanges
|
34
|
+
with_channel_check name do
|
35
|
+
@built_exchanges[name]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def disconnect
|
40
|
+
super
|
41
|
+
client.stop
|
42
|
+
end
|
43
|
+
|
44
|
+
# Subscribe for messages coming from Live Stream with name 'name'
|
45
|
+
# Each subscriber will receive a copy of messages flowing through the Live Stream
|
46
|
+
def message_from(name, options = {}, &block)
|
47
|
+
receive_message_from name, options, &block
|
48
|
+
end
|
49
|
+
|
50
|
+
# Subscribe for messages coming from cache of Live Stream with name 'name'
|
51
|
+
# The Live Stream will dispatch a message to the first ready subscriber in a round-robin fashion.
|
52
|
+
def message_from_cache(name, options = {}, &block)
|
53
|
+
options[:from_cache] = true
|
54
|
+
receive_message_from name, options, &block
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def check_client
|
60
|
+
raise ClientNotConnected, 'Client not connected. Did you call client.connect?' unless client_connected?
|
61
|
+
end
|
62
|
+
|
63
|
+
def client_connected?
|
64
|
+
client && client.status.eql?(:open)
|
65
|
+
end
|
66
|
+
|
67
|
+
def live_stream_data_from_name(name)
|
68
|
+
# Find the live_stream from provided name
|
69
|
+
unless live_stream_data = live_streams.find { |ls| ls[:name] == name }
|
70
|
+
raise LiveStreamNotFound.new(name)
|
71
|
+
end
|
72
|
+
live_stream_data
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse_ack(ack)
|
76
|
+
to_ack = false
|
77
|
+
auto_ack = false
|
78
|
+
if ack
|
79
|
+
raise AckTypeError unless ACK_TYPES.include?(ack)
|
80
|
+
to_ack = true
|
81
|
+
case ack
|
82
|
+
when :manual
|
83
|
+
auto_ack = false
|
84
|
+
when :auto
|
85
|
+
auto_ack = true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
return to_ack, auto_ack
|
89
|
+
end
|
90
|
+
|
91
|
+
def receive_message_from(name, options)
|
92
|
+
unless block_given?
|
93
|
+
raise BlockRequired
|
94
|
+
end
|
95
|
+
name = name.to_s
|
96
|
+
blocking = options.fetch :wait, false
|
97
|
+
to_ack, auto_ack = parse_ack options.fetch(:ack, :manual)
|
98
|
+
from_cache = options.fetch :from_cache, false
|
99
|
+
|
100
|
+
check_client
|
101
|
+
ls_channel = client.create_channel
|
102
|
+
live_stream_name = "#{live_stream_data_from_name(name)[:id]}.live_stream"
|
103
|
+
if from_cache
|
104
|
+
live_stream = ls_channel.queue live_stream_name, DEFAULT_QUEUE_OPTIONS
|
105
|
+
else
|
106
|
+
ls_exchange = ls_channel.fanout live_stream_name, DEFAULT_EXCHANGE_OPTIONS
|
107
|
+
live_stream = ls_channel.queue("#{client}_#{Time.now.to_f}.live_stream.temp", auto_delete: true)
|
108
|
+
.bind ls_exchange, routing_key: '#'
|
109
|
+
end
|
110
|
+
|
111
|
+
live_stream.subscribe(block: blocking, manual_ack: to_ack) do |delivery_info, metadata, payload|
|
112
|
+
message = LiveStream::Message.new ls_channel, options, delivery_info, metadata, payload
|
113
|
+
|
114
|
+
yield message
|
115
|
+
|
116
|
+
# If ack is :auto then ack current message
|
117
|
+
if to_ack && auto_ack
|
118
|
+
message.ack
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'http'
|
3
|
+
|
4
|
+
module Spacebunny
|
5
|
+
module LiveStream
|
6
|
+
|
7
|
+
# Proxy to Base.new
|
8
|
+
def self.new(*args)
|
9
|
+
Amqp.new *args
|
10
|
+
end
|
11
|
+
|
12
|
+
class Base
|
13
|
+
attr_accessor :api_endpoint, :auto_recover, :raise_on_error, :client, :secret, :host, :vhost, :live_streams
|
14
|
+
attr_reader :log_to, :log_level, :logger, :custom_connection_configs, :auto_connection_configs,
|
15
|
+
:connection_configs, :auto_configs, :tls, :tls_cert, :tls_key, :tls_ca_certificates, :verify_peer
|
16
|
+
|
17
|
+
def initialize(protocol, *args)
|
18
|
+
@protocol = protocol
|
19
|
+
@custom_connection_configs = {}
|
20
|
+
@auto_connection_configs = {}
|
21
|
+
options = args.extract_options.deep_symbolize_keys
|
22
|
+
|
23
|
+
@client = options[:client] || raise(ClientRequired)
|
24
|
+
@secret = options[:secret] || raise(SecretRequired)
|
25
|
+
@api_endpoint = options[:api_endpoint] || {}
|
26
|
+
|
27
|
+
extract_custom_connection_configs_from options
|
28
|
+
set_live_streams options[:live_streams]
|
29
|
+
|
30
|
+
@raise_on_error = options[:raise_on_error]
|
31
|
+
@log_to = options[:log_to] || STDOUT
|
32
|
+
@log_level = options[:log_level] || ::Logger::WARN
|
33
|
+
@logger = options[:logger] || build_logger
|
34
|
+
end
|
35
|
+
|
36
|
+
def api_endpoint=(options)
|
37
|
+
unless options.is_a? Hash
|
38
|
+
raise ArgumentError, 'api_endpoint must be an Hash. See doc for further info'
|
39
|
+
end
|
40
|
+
@api_endpoint = options.deep_symbolize_keys
|
41
|
+
end
|
42
|
+
|
43
|
+
def connection_configs
|
44
|
+
return @connection_configs if @connection_configs
|
45
|
+
if auto_configure?
|
46
|
+
# If key is specified, retrieve configs from APIs endpoint
|
47
|
+
@auto_configs = EndpointConnection.new(@api_endpoint.merge(client: @client, secret: @secret)).configs
|
48
|
+
check_and_add_live_streams @auto_configs[:live_streams]
|
49
|
+
@auto_connection_configs = normalize_auto_connection_configs
|
50
|
+
end
|
51
|
+
# Build final connection_configs
|
52
|
+
@connection_configs = merge_connection_configs
|
53
|
+
# Check for required params presence
|
54
|
+
check_connection_configs
|
55
|
+
@connection_configs
|
56
|
+
end
|
57
|
+
|
58
|
+
def connect
|
59
|
+
logger.warn "connect method must be implemented on class responsibile to handle protocol '#{@protocol}'"
|
60
|
+
end
|
61
|
+
|
62
|
+
def connection_options=(options)
|
63
|
+
unless options.is_a? Hash
|
64
|
+
raise ArgumentError, 'connection_options must be an Hash. See doc for further info'
|
65
|
+
end
|
66
|
+
extract_custom_connection_configs_from options.with_indifferent_access
|
67
|
+
end
|
68
|
+
|
69
|
+
def disconnect
|
70
|
+
@connection_configs = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# Stub method: must be implemented on the class responsible to handle the protocol
|
74
|
+
def on_receive(options = {})
|
75
|
+
logger.warn "on_receive method must be implemented on class responsibile to handle protocol '#{@protocol}'"
|
76
|
+
end
|
77
|
+
|
78
|
+
def auto_configure?
|
79
|
+
@client && @secret
|
80
|
+
end
|
81
|
+
|
82
|
+
def auto_recover
|
83
|
+
connection_configs[:auto_recover]
|
84
|
+
end
|
85
|
+
|
86
|
+
def host
|
87
|
+
connection_configs[:host]
|
88
|
+
end
|
89
|
+
|
90
|
+
def secret
|
91
|
+
connection_configs[:secret]
|
92
|
+
end
|
93
|
+
|
94
|
+
def vhost
|
95
|
+
connection_configs[:vhost]
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# @private
|
101
|
+
# Check if live_streams are an array
|
102
|
+
def set_live_streams(live_streams)
|
103
|
+
if live_streams && !live_streams.is_a?(Array)
|
104
|
+
raise StreamsMustBeAnArray
|
105
|
+
end
|
106
|
+
check_and_add_live_streams(live_streams)
|
107
|
+
end
|
108
|
+
|
109
|
+
# @private
|
110
|
+
# Check for required params presence
|
111
|
+
def check_connection_configs
|
112
|
+
# Do nothing ATM
|
113
|
+
end
|
114
|
+
|
115
|
+
# @private
|
116
|
+
# Merge auto_connection_configs and custom_connection_configs
|
117
|
+
def merge_connection_configs
|
118
|
+
auto_connection_configs.merge(custom_connection_configs) do |key, old_val, new_val|
|
119
|
+
if new_val.nil?
|
120
|
+
old_val
|
121
|
+
else
|
122
|
+
new_val
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# @private
|
128
|
+
def build_logger
|
129
|
+
logger = ::Logger.new(@log_to)
|
130
|
+
logger.level = normalize_log_level
|
131
|
+
logger.progname = 'Spacebunny'
|
132
|
+
Spacebunny.logger = logger
|
133
|
+
end
|
134
|
+
|
135
|
+
# @private
|
136
|
+
def extract_custom_connection_configs_from(options)
|
137
|
+
@custom_connection_configs = options
|
138
|
+
# Auto_recover from connection.close by default
|
139
|
+
@custom_connection_configs[:auto_recover] = @custom_connection_configs.delete(:auto_recover) || true
|
140
|
+
@custom_connection_configs[:host] = @custom_connection_configs.delete :host
|
141
|
+
if @custom_connection_configs[:protocols] && custom_connection_configs[:protocols][@protocol]
|
142
|
+
@custom_connection_configs[:port] = @custom_connection_configs[:protocols][@protocol].delete :port
|
143
|
+
@custom_connection_configs[:tls_port] = @custom_connection_configs[:protocols][@protocol].delete :tls_port
|
144
|
+
end
|
145
|
+
@custom_connection_configs[:vhost] = @custom_connection_configs.delete :vhost
|
146
|
+
@custom_connection_configs[:client] = @custom_connection_configs.delete :client
|
147
|
+
@custom_connection_configs[:secret] = @custom_connection_configs.delete :secret
|
148
|
+
end
|
149
|
+
|
150
|
+
# @private
|
151
|
+
def check_and_add_live_streams(chs)
|
152
|
+
@live_streams = [] unless @live_streams
|
153
|
+
return unless chs
|
154
|
+
chs.each do |ch|
|
155
|
+
case ch
|
156
|
+
when Hash
|
157
|
+
ch.symbolize_keys!
|
158
|
+
# Check for required attributes
|
159
|
+
[:id, :name].each do |attr|
|
160
|
+
unless ch[attr]
|
161
|
+
raise LiveStreamParamError(ch[:name], attr)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
@live_streams << ch
|
165
|
+
else
|
166
|
+
raise LiveStreamFormatError
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# @private
|
172
|
+
# Translate from auto configs given by APIs endpoint to a common format
|
173
|
+
def normalize_auto_connection_configs
|
174
|
+
{
|
175
|
+
host: @auto_configs[:connection][:host],
|
176
|
+
port: @auto_configs[:connection][:protocols][@protocol][:port],
|
177
|
+
tls_port: @auto_configs[:connection][:protocols][@protocol][:tls_port],
|
178
|
+
vhost: @auto_configs[:connection][:vhost],
|
179
|
+
client: @auto_configs[:connection][:client],
|
180
|
+
secret: @auto_configs[:connection][:secret]
|
181
|
+
}
|
182
|
+
end
|
183
|
+
|
184
|
+
# @private
|
185
|
+
def normalize_log_level
|
186
|
+
case @log_level
|
187
|
+
when :debug, ::Logger::DEBUG, 'debug' then ::Logger::DEBUG
|
188
|
+
when :info, ::Logger::INFO, 'info' then ::Logger::INFO
|
189
|
+
when :warn, ::Logger::WARN, 'warn' then ::Logger::WARN
|
190
|
+
when :error, ::Logger::ERROR, 'error' then ::Logger::ERROR
|
191
|
+
when :fatal, ::Logger::FATAL, 'fatal' then ::Logger::FATAL
|
192
|
+
else
|
193
|
+
Logger::WARN
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Spacebunny
|
2
|
+
module LiveStream
|
3
|
+
class Message
|
4
|
+
attr_reader :live_stream, :sender_id, :channel_name, :delivery_info, :metadata, :payload
|
5
|
+
|
6
|
+
def initialize(live_stream, options, delivery_info, metadata, payload)
|
7
|
+
@live_stream = live_stream
|
8
|
+
@options = options
|
9
|
+
@delivery_info = delivery_info
|
10
|
+
@metadata = metadata
|
11
|
+
@payload = payload
|
12
|
+
|
13
|
+
set_sender_id_and_channel
|
14
|
+
end
|
15
|
+
|
16
|
+
def ack(options = {})
|
17
|
+
multiple = options.fetch :multiple, false
|
18
|
+
@live_stream.acknowledge @delivery_info.delivery_tag, multiple
|
19
|
+
end
|
20
|
+
|
21
|
+
def nack(options = {})
|
22
|
+
multiple = options.fetch :multiple, false
|
23
|
+
requeue = options.fetch :requeue, false
|
24
|
+
@live_stream.nack @delivery_info.delivery_tag, multiple, requeue
|
25
|
+
end
|
26
|
+
|
27
|
+
def from_api?
|
28
|
+
!@metadata[:headers].nil? && @metadata[:headers]['x-from-sb-api']
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def set_sender_id_and_channel
|
34
|
+
@sender_id, @channel_name = @delivery_info[:routing_key].split('.')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Spacebunny
|
2
|
+
# Log on multiple outputs at the same time
|
3
|
+
#
|
4
|
+
# Usage example:
|
5
|
+
#
|
6
|
+
# log_file = File.open("log/debug.log", "a")
|
7
|
+
# Logger.new Spacebunny::Logger.new(STDOUT, log_file)
|
8
|
+
|
9
|
+
class Logger
|
10
|
+
def initialize(*args)
|
11
|
+
@streams = []
|
12
|
+
args.each do |a|
|
13
|
+
case a
|
14
|
+
when String
|
15
|
+
# This is a file path
|
16
|
+
puts File.open(a, 'a+').inspect
|
17
|
+
@streams << File.open(a, 'a+')
|
18
|
+
else
|
19
|
+
@streams << a
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(*args)
|
25
|
+
@streams.each do |lo|
|
26
|
+
lo.write(*args)
|
27
|
+
lo.flush
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
@streams.each(&:close)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
class Hash
|
2
|
+
# Returns a new hash with all keys converted using the block operation.
|
3
|
+
#
|
4
|
+
# hash = { name: 'Rob', age: '28' }
|
5
|
+
#
|
6
|
+
# hash.transform_keys{ |key| key.to_s.upcase }
|
7
|
+
# # => {"NAME"=>"Rob", "AGE"=>"28"}
|
8
|
+
def transform_keys
|
9
|
+
return enum_for(:transform_keys) unless block_given?
|
10
|
+
result = self.class.new
|
11
|
+
each_key do |key|
|
12
|
+
result[yield(key)] = self[key]
|
13
|
+
end
|
14
|
+
result
|
15
|
+
end
|
16
|
+
|
17
|
+
# Destructively convert all keys using the block operations.
|
18
|
+
# Same as transform_keys but modifies +self+.
|
19
|
+
def transform_keys!
|
20
|
+
return enum_for(:transform_keys!) unless block_given?
|
21
|
+
keys.each do |key|
|
22
|
+
self[yield(key)] = delete(key)
|
23
|
+
end
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns a new hash with all keys converted to strings.
|
28
|
+
#
|
29
|
+
# hash = { name: 'Rob', age: '28' }
|
30
|
+
#
|
31
|
+
# hash.stringify_keys
|
32
|
+
# # => {"name"=>"Rob", "age"=>"28"}
|
33
|
+
def stringify_keys
|
34
|
+
transform_keys{ |key| key.to_s }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Destructively convert all keys to strings. Same as
|
38
|
+
# +stringify_keys+, but modifies +self+.
|
39
|
+
def stringify_keys!
|
40
|
+
transform_keys!{ |key| key.to_s }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
44
|
+
# they respond to +to_sym+.
|
45
|
+
#
|
46
|
+
# hash = { 'name' => 'Rob', 'age' => '28' }
|
47
|
+
#
|
48
|
+
# hash.symbolize_keys
|
49
|
+
# # => {:name=>"Rob", :age=>"28"}
|
50
|
+
def symbolize_keys
|
51
|
+
transform_keys{ |key| key.to_sym rescue key }
|
52
|
+
end
|
53
|
+
alias_method :to_options, :symbolize_keys
|
54
|
+
|
55
|
+
# Destructively convert all keys to symbols, as long as they respond
|
56
|
+
# to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
|
57
|
+
def symbolize_keys!
|
58
|
+
transform_keys!{ |key| key.to_sym rescue key }
|
59
|
+
end
|
60
|
+
alias_method :to_options!, :symbolize_keys!
|
61
|
+
|
62
|
+
# Validate all keys in a hash match <tt>*valid_keys</tt>, raising
|
63
|
+
# ArgumentError on a mismatch.
|
64
|
+
#
|
65
|
+
# Note that keys are treated differently than HashWithIndifferentAccess,
|
66
|
+
# meaning that string and symbol keys will not match.
|
67
|
+
#
|
68
|
+
# { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
|
69
|
+
# { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
|
70
|
+
# { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
71
|
+
def assert_valid_keys(*valid_keys)
|
72
|
+
valid_keys.flatten!
|
73
|
+
each_key do |k|
|
74
|
+
unless valid_keys.include?(k)
|
75
|
+
raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns a new hash with all keys converted by the block operation.
|
81
|
+
# This includes the keys from the root hash and from all
|
82
|
+
# nested hashes and arrays.
|
83
|
+
#
|
84
|
+
# hash = { person: { name: 'Rob', age: '28' } }
|
85
|
+
#
|
86
|
+
# hash.deep_transform_keys{ |key| key.to_s.upcase }
|
87
|
+
# # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
|
88
|
+
def deep_transform_keys(&block)
|
89
|
+
_deep_transform_keys_in_object(self, &block)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Destructively convert all keys by using the block operation.
|
93
|
+
# This includes the keys from the root hash and from all
|
94
|
+
# nested hashes and arrays.
|
95
|
+
def deep_transform_keys!(&block)
|
96
|
+
_deep_transform_keys_in_object!(self, &block)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns a new hash with all keys converted to strings.
|
100
|
+
# This includes the keys from the root hash and from all
|
101
|
+
# nested hashes and arrays.
|
102
|
+
#
|
103
|
+
# hash = { person: { name: 'Rob', age: '28' } }
|
104
|
+
#
|
105
|
+
# hash.deep_stringify_keys
|
106
|
+
# # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
|
107
|
+
def deep_stringify_keys
|
108
|
+
deep_transform_keys{ |key| key.to_s }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Destructively convert all keys to strings.
|
112
|
+
# This includes the keys from the root hash and from all
|
113
|
+
# nested hashes and arrays.
|
114
|
+
def deep_stringify_keys!
|
115
|
+
deep_transform_keys!{ |key| key.to_s }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
119
|
+
# they respond to +to_sym+. This includes the keys from the root hash
|
120
|
+
# and from all nested hashes and arrays.
|
121
|
+
#
|
122
|
+
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
|
123
|
+
#
|
124
|
+
# hash.deep_symbolize_keys
|
125
|
+
# # => {:person=>{:name=>"Rob", :age=>"28"}}
|
126
|
+
def deep_symbolize_keys
|
127
|
+
deep_transform_keys{ |key| key.to_sym rescue key }
|
128
|
+
end
|
129
|
+
|
130
|
+
# Destructively convert all keys to symbols, as long as they respond
|
131
|
+
# to +to_sym+. This includes the keys from the root hash and from all
|
132
|
+
# nested hashes and arrays.
|
133
|
+
def deep_symbolize_keys!
|
134
|
+
deep_transform_keys!{ |key| key.to_sym rescue key }
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
# support methods for deep transforming nested hashes and arrays
|
139
|
+
def _deep_transform_keys_in_object(object, &block)
|
140
|
+
case object
|
141
|
+
when Hash
|
142
|
+
object.each_with_object({}) do |(key, value), result|
|
143
|
+
result[yield(key)] = _deep_transform_keys_in_object(value, &block)
|
144
|
+
end
|
145
|
+
when Array
|
146
|
+
object.map {|e| _deep_transform_keys_in_object(e, &block) }
|
147
|
+
else
|
148
|
+
object
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def _deep_transform_keys_in_object!(object, &block)
|
153
|
+
case object
|
154
|
+
when Hash
|
155
|
+
object.keys.each do |key|
|
156
|
+
value = object.delete(key)
|
157
|
+
object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
|
158
|
+
end
|
159
|
+
object
|
160
|
+
when Array
|
161
|
+
object.map! {|e| _deep_transform_keys_in_object!(e, &block)}
|
162
|
+
else
|
163
|
+
object
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class Array
|
169
|
+
# Extracts options from a set of arguments. Removes and returns the last
|
170
|
+
# element in the array if it's a hash, otherwise returns a blank hash.
|
171
|
+
#
|
172
|
+
# def options(*args)
|
173
|
+
# args.extract_options!
|
174
|
+
# end
|
175
|
+
#
|
176
|
+
# options(1, 2) # => {}
|
177
|
+
# options(1, 2, a: :b) # => {:a=>:b}
|
178
|
+
def extract_options
|
179
|
+
if last.is_a?(Hash)
|
180
|
+
pop
|
181
|
+
else
|
182
|
+
{}
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|