eventhub-processor 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dea7fe8b1b08a77c68d736c1c2e2e72325df218a
4
- data.tar.gz: bc476eac97e284575a684ed0cfe61850d4ea9b8f
3
+ metadata.gz: d6896414dd331b83418d8bbf1e7ed6f6efc78f35
4
+ data.tar.gz: c7b8614fc58ba0cdef80a3532d94a645564d08ee
5
5
  SHA512:
6
- metadata.gz: dcce6e241aefab6b054f79d942969722fa04ea9162e86fbcefce05e396d39c69062785ae0a2eca6409dd4e0864d604bbf84092f977f80d7a7068b531cc0c044b
7
- data.tar.gz: be828faa80aaa01901a29918413f265956835686b5120402726eec913eded92203fe57760f1b4f744da63ebbc9f307efe22175c4382662ab6a4322c0d9ee5d09
6
+ metadata.gz: 46bc0b39fb711bab9835d507bd6bb1410a154d262c0a25fc9239eda0f7bdf79e70480adba332b5e6f4201b9e31ac293ac7861525ea4689807dc8b991203d3916
7
+ data.tar.gz: b67fe54f620f45b063721dd6510258b520ea5ec37c7edc83f8e4bbf2ed343588c864726a0f3bb2f6052b3b3a936840b4fb377856a1ae7fa31d4e3bd9ec480f26
@@ -4,18 +4,21 @@ module EventHub
4
4
  include Singleton
5
5
  include Helper
6
6
 
7
- attr_accessor :data
7
+ attr_accessor :data, :folder, :environment
8
8
 
9
9
  def initialize
10
10
  @data = nil
11
+ @folder = Dir.pwd
12
+ @environment = 'development'
11
13
  end
12
14
 
13
15
  def load_file(input, env='development')
14
16
  tmp = JSON.parse( IO.read(input))
15
17
  @data = tmp[env]
18
+ @environment = env
16
19
  true
17
20
  rescue => e
18
- EventHub.logger.info("Unexpected exception while loading configuration [#{input}]: #{format_raw_string(e.message)}")
21
+ EventHub.logger.info("Unexpected exception while loading configuration [#{input}]: #{format_string(e.message)}")
19
22
  false
20
23
  end
21
24
 
@@ -1,11 +1,11 @@
1
1
  module EventHub
2
2
 
3
- EVENT_HUB_QUEUE_INBOUND = 'inbound'
3
+ EH_X_INBOUND = 'event_hub.inbound'
4
4
 
5
5
  STATUS_INITIAL = 0 # initial status code
6
6
  STATUS_SUCCESS = 200 # compare with HTTP Status Code: Success
7
7
  STATUS_RETRIED = 300 # compare with HTTP Status Code: Multiple Choices
8
8
  STATUS_INVALID = 400 # compare with HTTP Status Code: Bad request
9
9
  STATUS_UNDELIVERABLE = 500 # comapre with HTTP Status Code: Server Error
10
-
10
+ STATUS_ERROR = 600 # Business Process Error
11
11
  end
@@ -0,0 +1,48 @@
1
+ module HashExtensions
2
+
3
+ module ClassMethods
4
+ end
5
+
6
+ module InstanceMethods
7
+ # get value from provided key path, e.g. hash.get(%w(event_hub plate.queue1 retry_s))
8
+ # "a" => { "b" => { "c" => { "value"}}}
9
+ def get(arg)
10
+ path = arg.is_a?(String) ? arg.split('.') : arg
11
+ path.inject(self,:[])
12
+ rescue NoMethodError
13
+ return nil
14
+ end
15
+
16
+ # set value from provided key path, e.h. hash.set('a.b.c','new value')
17
+ # if overwrite is false, value will be set if it was nil previously
18
+ def set(arg,value,overwrite=true)
19
+ *key_path, last = arg.is_a?(String) ? arg.split(".") : arg
20
+ if overwrite
21
+ key_path.inject(self) { |h,key| h.has_key?(key) ? h[key] : h[key]={}} [last] = value
22
+ else
23
+ key_path.inject(self) { |h,key| h.has_key?(key) ? h[key] : h[key]={}} [last] ||= value
24
+ end
25
+ end
26
+
27
+ # get all keys path, { 'a' => 'value1', 'b' => { 'c' => 'value2'}}.all_keys_with_path => ['a','b.c']
28
+ def all_keys_with_path(parent=nil)
29
+ a = []
30
+ each do |k,v|
31
+ if v.is_a?(Hash)
32
+ a << v.all_keys_with_path([parent,k].compact.join('.'))
33
+ else
34
+ a << "#{[parent,k].compact.join(".")}"
35
+ end
36
+ end
37
+ a.flatten
38
+ end
39
+
40
+ end
41
+
42
+ def self.included(receiver)
43
+ receiver.extend ClassMethods
44
+ receiver.send :include, InstanceMethods
45
+ end
46
+ end
47
+
48
+ HashExtensions.included(Hash)
@@ -2,18 +2,54 @@ module EventHub
2
2
 
3
3
  module Helper
4
4
 
5
- # converts a base class name, Whatever::MyClassName => my_class_name
6
- def class_to_string(class_name)
7
- class_name.to_s.split("::")[-1].gsub(/[A-Z]/) { |c| "_#{c}"}.gsub(/^_/,"").downcase
5
+ # converts a class like EventHub::PlateStore::MyClassName to an array ['event_hub','plate_store','my_class_name']
6
+ def class_to_array(class_name)
7
+ class_name.to_s.split("::").map{ |m| m.gsub(/[A-Z]/) { |c| "_#{c}"}.gsub(/^_/,"").downcase }
8
8
  end
9
9
 
10
- def format_raw_string(message,max_characters=80)
10
+ # replaces CR, LF, CRLF with ";" and cut's string to requied length by adding "..." if string would be longer
11
+ def format_string(message,max_characters=80)
11
12
  max_characters = 5 if max_characters < 5
12
13
  m = message.gsub(/\r\n|\n|\r/m,";")
13
- return (m[0..max_characters-3] + "...") if m.size > max_characters
14
+ return (m[0..max_characters-4] + "...") if m.size > max_characters
14
15
  return m
15
16
  end
16
17
 
18
+ def get_host
19
+ Socket.gethostname
20
+ end
21
+
22
+ def get_ip_adresses
23
+ list = Socket.ip_address_list.map { |i| i.ip_address unless i.ipv4_loopback? || i.ipv6_loopback? }.compact
24
+ return list.size == 0 ? ["no ip address found (loopback excluded)"] : list
25
+ end
26
+
27
+ def now_stamp(now=nil)
28
+ now ||= Time.now
29
+ now.utc.strftime("%Y-%m-%dT%H:%M:%S.#{now.usec}Z")
30
+ end
31
+
32
+ def duration(difference)
33
+ rest, secs = difference.divmod( 60 ) # self is the time difference t2 - t1
34
+ rest, mins = rest.divmod( 60 )
35
+ days, hours = rest.divmod( 24 )
36
+ secs = secs.truncate
37
+ milliseconds = ((difference - difference.truncate)*1000).round
38
+
39
+ result = []
40
+ result << "#{days} days" if days > 1
41
+ result << "#{days} day" if days == 1
42
+ result << "#{hours} hours" if hours > 1
43
+ result << "#{hours} hour" if hours == 1
44
+ result << "#{mins} minutes" if mins > 1
45
+ result << "#{mins} minute" if mins == 1
46
+ result << "#{secs} seconds" if secs > 1
47
+ result << "#{secs} second" if secs == 1
48
+ result << "#{milliseconds} milliseconds" if milliseconds > 1
49
+ result << "#{milliseconds} millisecond" if milliseconds == 1
50
+ return result.join(' ')
51
+ end
52
+
17
53
  end
18
54
 
19
55
  end
@@ -1,6 +1,7 @@
1
1
  module EventHub
2
2
 
3
3
  class Message
4
+ include Helper
4
5
 
5
6
  VERSION = '1.0.0'
6
7
 
@@ -50,23 +51,27 @@ module EventHub
50
51
  @raw = raw
51
52
 
52
53
  # set message defaults, that we have required headers
53
- now = Time.now
54
54
  @header.set('message_id',UUIDTools::UUID.timestamp_create.to_s,false)
55
55
  @header.set('version',VERSION,false)
56
- @header.set('created_at',now.utc.strftime("%Y-%m-%dT%H:%M:%S.#{now.usec/1000}Z"),false)
56
+ @header.set('created_at',now_stamp,false)
57
57
 
58
- @header.set('process.name',nil,false)
58
+ @header.set('origin.module_id','undefined',false)
59
+ @header.set('origin.type','undefined',false)
60
+ @header.set('origin.site_id','undefined',false)
61
+
62
+ @header.set('process.name','undefined',false)
59
63
  @header.set('process.execution_id',UUIDTools::UUID.timestamp_create.to_s,false)
60
64
  @header.set('process.step_position',0,false)
61
65
 
62
66
  @header.set('status.retried_count',0,false)
63
67
  @header.set('status.code',STATUS_INITIAL,false)
64
- @header.set('status.message',nil,false)
68
+ @header.set('status.message','',false)
65
69
 
66
70
  end
67
71
 
68
72
  def valid?
69
- REQUIRED_HEADERS.all? { |key| @header.all_keys_with_path.include?(key) }
73
+ # check for existence and defined value
74
+ REQUIRED_HEADERS.all? { |key| @header.all_keys_with_path.include?(key) && !!self.send(key.gsub(/\./,"_").to_sym)}
70
75
  end
71
76
 
72
77
  def success?
@@ -90,14 +95,19 @@ module EventHub
90
95
  end
91
96
 
92
97
  def to_s
93
- "Message: message_id [#{self.message_id}], status.code [#{status_code}], status.message [#{status_message}], status.retried_count [#{status_retried_count}] "
98
+ "Msg: process [#{self.process_name},#{self.process_step_position},#{self.process_execution_id}], status [#{self.status_code},#{self.status_message},#{self.status_retried_count}]"
94
99
  end
95
100
 
96
- def copy(args={})
97
- copied_header = self.header.dup
101
+ # copies the message and set's provided status code (default: success), actual stamp, and a new message id
102
+ def copy(status_code=STATUS_SUCCESS)
103
+
104
+ copied_header = self.header.dup
98
105
  copied_body = self.body.dup
99
106
 
100
- args.each { |key,value| copied_header.set(key,value) } if args.is_a?(Hash)
107
+ copied_header.set("message_id",UUIDTools::UUID.timestamp_create.to_s)
108
+ copied_header.set("created_at",now_stamp)
109
+ copied_header.set("status.code",status_code)
110
+
101
111
  Message.new(copied_header, copied_body)
102
112
  end
103
113
 
@@ -6,7 +6,7 @@ module EventHub
6
6
  include Helper
7
7
 
8
8
  def initialize(name=nil)
9
- @name = name || class_to_string(self.class)
9
+ @name = name || class_to_array(self.class)[1..-1].join(".")
10
10
  @folder = Dir.pwd
11
11
 
12
12
  @started = Time.now
@@ -21,28 +21,32 @@ module EventHub
21
21
  EventHub::Configuration.instance.data
22
22
  end
23
23
 
24
- def host
24
+ def server_host
25
25
  configuration.get('server.host') || 'localhost'
26
26
  end
27
27
 
28
- def user
28
+ def server_user
29
29
  configuration.get('server.user') || 'admin'
30
30
  end
31
31
 
32
- def password
32
+ def server_password
33
33
  configuration.get('server.password') || 'admin'
34
34
  end
35
35
 
36
- def management_port
36
+ def server_management_port
37
37
  configuration.get('server.management_port') || 15672
38
38
  end
39
39
 
40
- def listener_queue
41
- configuration.get('processor.listener_queue') || 'inbound'
40
+ def server_vhost
41
+ configuration.get('server.vhost') || 'event_hub'
42
42
  end
43
43
 
44
- def vhost
45
- configuration.get('server.vhost') || nil
44
+ def connection_settings
45
+ { user: server_user, password: server_password, host: server_host, vhost: server_vhost }
46
+ end
47
+
48
+ def listener_queue
49
+ configuration.get('processor.listener_queue') || 'undefined_listener_queue'
46
50
  end
47
51
 
48
52
  def watchdog_cycle_in_s
@@ -60,42 +64,64 @@ module EventHub
60
64
  def start(detached=false)
61
65
  daemonize if detached
62
66
 
67
+ EventHub.logger.info("Processor [#{@name}] base folder [#{@folder}]")
68
+
69
+ channel = nil
70
+
63
71
  while @restart
64
72
 
65
73
  begin
66
- AMQP.start(configuration.get('server')) do |connection, open_ok|
74
+ AMQP.start(self.connection_settings) do |connection, open_ok|
67
75
 
68
76
  @connection = connection
69
77
 
70
78
  # deal with tcp connection issues
71
79
  @connection.on_tcp_connection_loss do |conn, settings|
72
- EventHub.logger.warn("Processor lost tcp connection. Trying to restart in #{@restart_in_s} seconds...")
80
+ EventHub.logger.warn("Processor lost tcp connection. Trying to restart in #{self.restart_in_s} seconds...")
73
81
  stop_processor(true)
74
82
  end
75
83
 
76
84
  # create channel
77
- @channel = AMQP::Channel.new(@connection)
78
- @channel.auto_recovery = true
85
+ channel = AMQP::Channel.new(@connection)
79
86
 
80
87
  # connect to queue
81
- @queue = @channel.queue(self.listener_queue, durable: true, auto_delete: false)
88
+ @queue = channel.queue(self.listener_queue, durable: true, auto_delete: false)
82
89
 
83
90
  # subscribe to queue
84
- @queue.subscribe(:ack => true) do |metadata, payload|
91
+ @queue.subscribe(:ack => true) do |metadata, payload|
92
+
93
+
85
94
  begin
86
- if handle_message(metadata,payload)
87
- raise
88
- metadata.ack
89
- else
90
- metadata.nack
95
+ messages_to_send = []
96
+
97
+ # try to convert to Evenhub message
98
+ message = Message.from_json(payload)
99
+ EventHub.logger.info("-> #{message.to_s}")
100
+
101
+ if message.status_code == STATUS_INVALID
102
+ messages_to_send << message
103
+ EventHub.logger.info("-> #{message.to_s} => Put to queue [#{EH_X_INBOUND}].")
104
+ else
105
+ # pass received message to handler or dervied handler
106
+ messages_to_send = Array(handle_message(message))
107
+ end
108
+
109
+ # forward invalid or returned messages to dispatcher
110
+ messages_to_send.each do |message|
111
+ send_message(message)
91
112
  end
113
+ channel.acknowledge(metadata.delivery_tag)
114
+ @messages_successful += 1
115
+
92
116
  rescue => e
93
- EventHub.logger.error("Unexpected exception in handle_message method: #{e}")
117
+ channel.reject(metadata.delivery_tag,false)
118
+ @messages_unsuccessful += 1
119
+ EventHub.logger.error("Unexpected exception in handle_message method: #{e}. Message dead lettered.")
94
120
  EventHub.logger.save_detailed_error(e)
95
121
  end
96
122
  end
97
123
 
98
- EventHub.logger.info("Processor [#{@name}] is listening to queue [#{self.listener_queue}], base folder [#{@folder}]")
124
+ EventHub.logger.info("Processor [#{@name}] is listening to vhost [#{self.server_vhost}], queue [#{self.listener_queue}]")
99
125
 
100
126
  # Singnal Listening
101
127
  Signal.trap("TERM") {stop_processor}
@@ -111,7 +137,7 @@ module EventHub
111
137
  Signal.trap("INT") { stop_processor }
112
138
 
113
139
  id = EventHub.logger.save_detailed_error(e)
114
- EventHub.logger.error("Unexpected exception: #{e}, see => #{id}")
140
+ EventHub.logger.error("Unexpected exception: #{e}, see => #{id}. Trying to restart in #{self.restart_in_s} seconds...")
115
141
 
116
142
  sleep_break self.restart_in_s
117
143
  end
@@ -134,15 +160,16 @@ module EventHub
134
160
 
135
161
  def watchdog
136
162
  begin
137
- response = RestClient.get "http://#{self.user}:#{self.password}@#{self.host}:#{self.management_port}/api/queues/#{self.vhost}/#{self.listener_queue}/bindings", { :content_type => :json}
163
+ response = RestClient.get "http://#{self.server_user}:#{self.server_password}@#{self.server_host}:#{self.server_management_port}/api/queues/#{self.server_vhost}/#{self.listener_queue}/bindings", { :content_type => :json}
138
164
  data = JSON.parse(response.body)
139
165
 
140
166
  if response.code != 200
141
167
  EventHub.logger.warn("Watchdog: Server did not answered properly. Trying to restart in #{self.restart_in_s} seconds...")
142
- stop_processor
168
+ EventMachine.add_timer(self.restart_in_s) { stop_processor(true) }
143
169
  elsif data.size == 0
144
170
  EventHub.logger.warn("Watchdog: Something is wrong with the vhost, queue, and/or bindings. Trying to restart in #{self.restart_in_s} seconds...")
145
- stop_processor
171
+ EventMachine.add_timer(self.restart_in_s) { stop_processor(true) }
172
+ # does it make sence ? Needs maybe more checks in future
146
173
  else
147
174
  # Watchdog is happy :-)
148
175
  # add timer for next check
@@ -159,16 +186,20 @@ module EventHub
159
186
  message = Message.new
160
187
  message.origin_module_id = @name
161
188
  message.origin_type = "processor"
162
- message.origin_site_id = 'chbs'
189
+ message.origin_site_id = 'global'
163
190
 
164
191
  message.process_name = 'event_hub.heartbeat'
165
192
 
193
+ now = Time.now
166
194
  message.body = {
167
195
  heartbeat: {
168
- started: @started,
169
- stamp_last_beat: Time.now,
196
+ started: now_stamp(@started),
197
+ stamp_last_beat: now_stamp(now),
198
+ uptime: duration(now-@started),
170
199
  heartbeat_cycle_in_s: self.heartbeat_cycle_in_s,
171
200
  served_queues: [self.listener_queue],
201
+ host: get_host,
202
+ ip_adresses: get_ip_adresses,
172
203
  messages: {
173
204
  total: @messages_successful+@messages_unsuccessful,
174
205
  successful: @messages_successful,
@@ -177,35 +208,21 @@ module EventHub
177
208
  }
178
209
  }
179
210
 
180
- send_to_dispatcher(message.to_json)
211
+ # send heartbeat message
212
+ send_message(message)
181
213
 
182
214
  EventMachine.add_timer(self.heartbeat_cycle_in_s) { heartbeat }
183
215
  end
184
216
 
185
- def send_to_dispatcher(payload)
186
- confirmed = true
187
-
188
- connection = Bunny.new({hostname: self.host, user: self.user, password: self.password, vhost: "event_hub"})
189
- connection.start
190
-
191
- channel = connection.create_channel
192
- channel.confirm_select
193
-
194
- channel.default_exchange.publish(payload,routing_key: EVENT_HUB_QUEUE_INBOUND, persistent: true)
195
- success = channel.wait_for_confirms
196
-
197
- if !success
198
- EventHub.logger.error("Message has not been confirmed by the server to be received !!!")
199
- confirmed = false
200
- @messages_unsuccessful += 1
201
- else
202
- @messages_successful += 1
203
- end
204
-
205
- channel.close
206
- connection.close
207
-
208
- confirmed
217
+ # send message
218
+ def send_message(message,exchange_name=EH_X_INBOUND)
219
+ AMQP::Channel.new(@connection) do |channel|
220
+ channel.direct(exchange_name, :durable => true, :auto_delete => false) do |exchange, declare_ok|
221
+ exchange.publish(message.to_json, :persistent => true)
222
+ end
223
+ end
224
+ rescue => e
225
+ EventHub.logger.error("Unexpected exception while sending message to [#{exchange_name}]: #{e}")
209
226
  end
210
227
 
211
228
  def sleep_break( seconds ) # breaks after n seconds or after interrupt
@@ -221,11 +238,11 @@ module EventHub
221
238
  def stop_processor(restart=false)
222
239
  @restart = restart
223
240
 
224
- # stop event loop
225
- @connection.disconnect {
226
- EventHub.logger.info("Processor [#{@name}] is stopping main event loop")
227
- EventMachine.stop
228
- }
241
+ # stop connection and event loop
242
+ if @connection
243
+ @connection.disconnect if @connection.connected?
244
+ EventMachine.stop if EventMachine.reactor_running?
245
+ end
229
246
  end
230
247
 
231
248
  def daemonize
@@ -1,3 +1,3 @@
1
1
  module EventHub
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
@@ -1,10 +1,10 @@
1
1
  require 'amqp'
2
- require 'bunny'
3
2
  require 'rest-client'
4
3
  require 'json'
5
4
  require 'singleton'
6
5
  require 'uuidtools'
7
6
  require 'base64'
7
+ require 'socket'
8
8
 
9
9
  require_relative 'eventhub/version'
10
10
  require_relative 'eventhub/constant'
@@ -12,7 +12,7 @@ require_relative 'eventhub/helper'
12
12
  require_relative 'eventhub/multi_logger'
13
13
 
14
14
  require_relative 'eventhub/configuration'
15
- require_relative 'eventhub/hash'
15
+ require_relative 'eventhub/hash_extensions'
16
16
  require_relative 'eventhub/processor'
17
17
  require_relative 'eventhub/message'
18
18
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eventhub-processor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Steiner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-04-03 00:00:00.000000000 Z
11
+ date: 2014-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -39,13 +39,13 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rest-client
42
+ name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
- type: :runtime
48
+ type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: amqp
56
+ name: rest-client
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: bunny
70
+ name: amqp
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -102,11 +102,10 @@ extensions: []
102
102
  extra_rdoc_files: []
103
103
  files:
104
104
  - LICENSE.txt
105
- - Rakefile
106
105
  - lib/eventhub-processor.rb
107
106
  - lib/eventhub/configuration.rb
108
107
  - lib/eventhub/constant.rb
109
- - lib/eventhub/hash.rb
108
+ - lib/eventhub/hash_extensions.rb
110
109
  - lib/eventhub/helper.rb
111
110
  - lib/eventhub/message.rb
112
111
  - lib/eventhub/multi_logger.rb
data/Rakefile DELETED
@@ -1 +0,0 @@
1
- require "bundler/gem_tasks"
data/lib/eventhub/hash.rb DELETED
@@ -1,34 +0,0 @@
1
- class ::Hash
2
-
3
- # get value from provided key path, e.g. hash.get('a.b.c')
4
- def get(key_path)
5
- key_path.split('.').inject(self,:[])
6
- rescue NoMethodError
7
- return nil
8
- end
9
-
10
- # set value from provided key path, e.h. hash.set('a.b.c','new value')
11
- # if overwrite is false, value will be set if it was nil previously
12
- def set(key_path,value,overwrite=true)
13
- *key_path, last = key_path.split(".")
14
- if overwrite
15
- key_path.inject(self) { |h,key| h.has_key?(key) ? h[key] : h[key]={}} [last] = value
16
- else
17
- key_path.inject(self) { |h,key| h.has_key?(key) ? h[key] : h[key]={}} [last] ||= value
18
- end
19
- end
20
-
21
- # get all keys path, { 'a' => 'value1', 'b' => { 'c' => 'value2'}}.all_keys_with_path => ['a','b.c']
22
- def all_keys_with_path(parent=nil)
23
- a = []
24
- each do |k,v|
25
- if v.is_a?(Hash)
26
- a << v.all_keys_with_path([parent,k].compact.join('.'))
27
- else
28
- a << "#{[parent,k].compact.join(".")}"
29
- end
30
- end
31
- a.flatten
32
- end
33
-
34
- end