omf_common 6.0.0.pre.10 → 6.0.0.pre.11

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.
Files changed (41) hide show
  1. data/bin/monitor_topic.rb +80 -0
  2. data/bin/send_create.rb +94 -0
  3. data/bin/send_request.rb +58 -0
  4. data/example/engine_alt.rb +136 -0
  5. data/example/vm_alt.rb +65 -0
  6. data/lib/omf_common.rb +224 -3
  7. data/lib/omf_common/comm.rb +113 -46
  8. data/lib/omf_common/comm/amqp/amqp_communicator.rb +76 -0
  9. data/lib/omf_common/comm/amqp/amqp_topic.rb +91 -0
  10. data/lib/omf_common/comm/local/local_communicator.rb +64 -0
  11. data/lib/omf_common/comm/local/local_topic.rb +42 -0
  12. data/lib/omf_common/comm/topic.rb +190 -0
  13. data/lib/omf_common/{dsl/xmpp.rb → comm/xmpp/communicator.rb} +93 -53
  14. data/lib/omf_common/comm/xmpp/topic.rb +147 -0
  15. data/lib/omf_common/{dsl → comm/xmpp}/xmpp_mp.rb +2 -0
  16. data/lib/omf_common/eventloop.rb +94 -0
  17. data/lib/omf_common/eventloop/em.rb +57 -0
  18. data/lib/omf_common/eventloop/local_evl.rb +78 -0
  19. data/lib/omf_common/message.rb +112 -229
  20. data/lib/omf_common/message/json/json_message.rb +129 -0
  21. data/lib/omf_common/message/xml/message.rb +410 -0
  22. data/lib/omf_common/message/xml/relaxng_schema.rb +17 -0
  23. data/lib/omf_common/message/xml/topic_message.rb +20 -0
  24. data/lib/omf_common/protocol/6.0.rnc +11 -21
  25. data/lib/omf_common/protocol/6.0.rng +52 -119
  26. data/lib/omf_common/version.rb +1 -1
  27. data/omf_common.gemspec +4 -2
  28. data/test/fixture/pubsub.rb +19 -19
  29. data/test/omf_common/{dsl/xmpp_spec.rb → comm/xmpp/communicator_spec.rb} +47 -111
  30. data/test/omf_common/comm/xmpp/topic_spec.rb +113 -0
  31. data/test/omf_common/comm_spec.rb +1 -0
  32. data/test/omf_common/message/xml/message_spec.rb +136 -0
  33. data/test/omf_common/message_spec.rb +37 -131
  34. data/test/test_helper.rb +4 -1
  35. metadata +38 -28
  36. data/lib/omf_common/core_ext/object.rb +0 -21
  37. data/lib/omf_common/relaxng_schema.rb +0 -17
  38. data/lib/omf_common/topic.rb +0 -34
  39. data/lib/omf_common/topic_message.rb +0 -20
  40. data/test/omf_common/topic_message_spec.rb +0 -114
  41. data/test/omf_common/topic_spec.rb +0 -75
@@ -0,0 +1,80 @@
1
+ require 'optparse'
2
+
3
+ DESCR = %{
4
+ Monitor a set of resources (topics) and print all observed messages.
5
+
6
+ If the 'follow-children' flag is set, automatically add all resources
7
+ created by the monitored resources to the monitor set. Please note
8
+ that there will be a delay until the new monitors are in place which
9
+ can result in missed messages.
10
+ }
11
+
12
+ require 'omf_common'
13
+
14
+ OP_MODE = :development
15
+
16
+ opts = {
17
+ communication: {
18
+ url: 'amqp://srv.mytestbed.net'
19
+ },
20
+ eventloop: { type: :em},
21
+ logging: {
22
+ level: 'info'
23
+ }
24
+ }
25
+
26
+ observed_topic = nil
27
+ $follow_children = true
28
+
29
+ op = OptionParser.new
30
+ op.banner = "Usage: #{op.program_name} [options] topic1 topic2 ...\n#{DESCR}\n"
31
+ op.on '-c', '--comms-url URL', "URL to communication layer [#{opts[:communication][:url]}]" do |url|
32
+ opts[:communication][:url] = url
33
+ end
34
+ op.on '-f', "--[no-]follow-children", "Follow all newly created resources [#{$follow_children}]" do |flag|
35
+ $follow_children = flag
36
+ end
37
+ op.on '-d', '--debug', "Set logging to DEBUG level" do
38
+ opts[:logging][:level] = 'debug'
39
+ end
40
+ op.on_tail('-h', "--help", "Show this message") { $stderr.puts op; exit }
41
+ observed_topics = op.parse(ARGV)
42
+
43
+ unless observed_topics
44
+ $stderr.puts 'Missing declaration of topics to follow'
45
+ $stderr.puts op
46
+ exit(-1)
47
+ end
48
+
49
+ $observed_topics = {}
50
+
51
+ def observe(tname, comm)
52
+ return if $observed_topics.key? tname
53
+
54
+ info "Observing '#{tname}'"
55
+ $observed_topics[tname] = true
56
+ comm.subscribe(tname) do |topic|
57
+ topic.on_message do |msg|
58
+ ts = Time.now.strftime('%H:%M:%S')
59
+ puts "#{ts} #{msg.type}(#{msg.itype}) #{msg.inspect}"
60
+ puts " #{topic.id}"
61
+ msg.each_property do |name, value|
62
+ puts " #{name}: #{value}"
63
+ end
64
+ puts "------"
65
+
66
+ if $follow_children && msg.itype == 'creation_ok'
67
+ #puts ">>>>>> #{msg}"
68
+ observe(msg[:res_id], comm)
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ OmfCommon.init(OP_MODE, opts) do |el|
75
+ OmfCommon.comm.on_connected do |comm|
76
+ observed_topics.each do |topic|
77
+ observe(topic, comm)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,94 @@
1
+ #
2
+ DESCR = %{
3
+ Send a create to a specific resource (topic) and print out any replies.
4
+
5
+ Any additional command line arguments are interpreted as paramters to
6
+ the create.
7
+ }
8
+
9
+ require 'omf_common'
10
+
11
+ OP_MODE = :development
12
+
13
+ opts = {
14
+ communication: {
15
+ # url: 'amqp://srv.mytestbed.net'
16
+ },
17
+ eventloop: { type: :em},
18
+ logging: {
19
+ level: 'info'
20
+ }
21
+ }
22
+
23
+ resource_url = nil
24
+ resource_type = nil
25
+
26
+ op = OptionParser.new
27
+ op.banner = "Usage: #{op.program_name} [options] type pname1: val1 pname2: val2 ...\n#{DESCR}\n"
28
+ op.on '-r', '--resource-url URL', "URL of resource" do |url|
29
+ resource_url = url
30
+ end
31
+ op.on '-t', '--type TYPE', "Type of resource to create" do |type|
32
+ resource_type = type
33
+ end
34
+ op.on '-d', '--debug', "Set logging to DEBUG level" do
35
+ opts[:logging][:level] = 'debug'
36
+ end
37
+ op.on_tail('-h', "--help", "Show this message") { $stderr.puts op; exit }
38
+ rest = op.parse(ARGV) || []
39
+
40
+ unless resource_url || resource_type
41
+ $stderr.puts 'Missing --resource-url --type or'
42
+ $stderr.puts op
43
+ exit(-1)
44
+ end
45
+
46
+ r = resource_url.split('/')
47
+ resource = r.pop
48
+ opts[:communication][:url] = r.join('/')
49
+
50
+ copts = {}
51
+ key = nil
52
+ def err_exit
53
+ $stderr.puts("Options need to be of the 'key: value' type")
54
+ exit(-1)
55
+ end
56
+ rest.each do |s|
57
+ sa = s.split(':')
58
+ if sa.length == 2
59
+ err_exit if key
60
+ copts[sa[0]] = sa[1]
61
+ else
62
+ if s.end_with?(':')
63
+ err_exit if key
64
+ key = s[0]
65
+ else
66
+ err_exit unless key
67
+ copts[key] = s[0]
68
+ key = nil
69
+ end
70
+ end
71
+ end
72
+ err_exit if key
73
+
74
+ OmfCommon.init(OP_MODE, opts) do |el|
75
+ OmfCommon.comm.on_connected do |comm|
76
+ comm.subscribe(resource) do |topic|
77
+ # topic.on_inform do |msg|
78
+ # puts "#{resource} <#{msg.type}(#{msg.itype})> #{msg.inspect}"
79
+ # msg.each_property do |name, value|
80
+ # puts " #{name}: #{value}"
81
+ # end
82
+ # puts "------"
83
+ # end
84
+
85
+ topic.create(resource_type, copts) do |msg|
86
+ puts "#{resource} <#{msg.type}(#{msg.itype})> #{msg.inspect}"
87
+ msg.each_property do |name, value|
88
+ puts " #{name}: #{value}"
89
+ end
90
+ puts "------"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,58 @@
1
+ #
2
+ DESCR = %{
3
+ Send a request to a specific resource (topic) and print out any replies.
4
+
5
+ Any additional command line arguments are interpreted as limiting the request
6
+ to those, otherwise all properties are requested.
7
+ }
8
+
9
+ require 'omf_common'
10
+
11
+ OP_MODE = :development
12
+
13
+ opts = {
14
+ communication: {
15
+ # url: 'amqp://srv.mytestbed.net'
16
+ },
17
+ eventloop: { type: :em},
18
+ logging: {
19
+ level: 'info'
20
+ }
21
+ }
22
+
23
+ resource_url = nil
24
+
25
+ op = OptionParser.new
26
+ op.banner = "Usage: #{op.program_name} [options] prop1 prop2 ...\n#{DESCR}\n"
27
+ op.on '-r', '--resource-url URL', "URL of resource" do |url|
28
+ resource_url = url
29
+ end
30
+ op.on '-d', '--debug', "Set logging to DEBUG level" do
31
+ opts[:logging][:level] = 'debug'
32
+ end
33
+ op.on_tail('-h', "--help", "Show this message") { $stderr.puts op; exit }
34
+ req_properties = op.parse(ARGV) || []
35
+
36
+ unless resource_url
37
+ $stderr.puts 'Missing --resource-url'
38
+ $stderr.puts op
39
+ exit(-1)
40
+ end
41
+
42
+ r = resource_url.split('/')
43
+ resource = r.pop
44
+ opts[:communication][:url] = r.join('/')
45
+
46
+ OmfCommon.init(OP_MODE, opts) do |el|
47
+ OmfCommon.comm.on_connected do |comm|
48
+ comm.subscribe(resource) do |topic|
49
+ topic.request(req_properties) do |msg|
50
+ puts "#{resource} <#{msg.type}(#{msg.itype})> #{msg.inspect}"
51
+ msg.each_property do |name, value|
52
+ puts " #{name}: #{value}"
53
+ end
54
+ puts "------"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,136 @@
1
+ # OMF_VERSIONS = 6.0
2
+ require 'omf_common'
3
+
4
+ opts = {
5
+ communication: {
6
+ url: 'amqp://localhost',
7
+ }
8
+ }
9
+
10
+ # $stdout.sync = true
11
+ # Logging.appenders.stdout(
12
+ # 'my_format',
13
+ # :layout => Logging.layouts.pattern(:date_pattern => '%H:%M:%S',
14
+ # :pattern => '%d %5l %c{2}: %m\n',
15
+ # :color_scheme => 'none'))
16
+ # Logging.logger.root.appenders = 'my_format'
17
+ # Logging.logger.root.level = :debug if opts[:debug]
18
+
19
+ # Environment setup
20
+ #OmfCommon.init(:developement, opts)
21
+ OmfCommon.init(:local)
22
+
23
+
24
+
25
+ def create_engine(garage)
26
+ garage.create(:engine, name: 'mp4') do |msg|
27
+ if msg.success?
28
+ engine = msg.resource
29
+ on_engine_created(engine, garage)
30
+ else
31
+ logger.error "Resource creation failed - #{msg[:reason]}"
32
+ end
33
+ end
34
+ end
35
+
36
+
37
+ # This is an alternative version of creating a new engine.
38
+ # We create teh message first without sending it, then attach various
39
+ # response handlers and finally publish it.
40
+ #
41
+ # TODO: This is most likely NOT working yet
42
+ #
43
+ def create_engine2
44
+ msg = garage.create_message('mp4')
45
+ msg.on_created do |engine, emsg|
46
+ on_engine_created(engine, garage)
47
+ end
48
+ msg.on_created_failed do |fmsg|
49
+ logger.error "Resource creation failed - #{msg[:reason]}"
50
+ end
51
+ msg.publish
52
+ end
53
+
54
+ # This method is called whenever a new engine has been created by the garage.
55
+ #
56
+ # @param [Topic] engine Topic representing the created engine
57
+ #
58
+ def on_engine_created(engine, garage)
59
+ # Monitor all status information from teh engine
60
+ engine.on_inform_status do |msg|
61
+ msg.each_property do |name, value|
62
+ logger.info "#{name} => #{value}"
63
+ end
64
+ end
65
+
66
+ engine.on_inform_failed do |msg|
67
+ logger.error msg.read_content("reason")
68
+ end
69
+
70
+ # Send a request for specific properties
71
+ puts ">>> SENDING REQUEST"
72
+ engine.request([:max_rpm, {:provider => {country: 'japan'}}, :max_power]) do |msg|
73
+ #engine.request([:max_rpm, :max_power])
74
+ #engine.request() do |msg|
75
+ puts ">>> REPLY #{msg.inspect}"
76
+ end
77
+
78
+
79
+
80
+
81
+ return
82
+
83
+ # Now we will apply 50% throttle to the engine
84
+ engine.configure(throttle: 50)
85
+
86
+ # Some time later, we want to reduce the throttle to 0, to avoid blowing up the engine
87
+ engine.after(5) do
88
+ engine.configure(throttle: 0)
89
+
90
+ # While we are at it, also test error handling
91
+ engine.request([:error]) do |msg|
92
+ if msg.success?
93
+ logger.error "Expected unsuccessful reply"
94
+ else
95
+ logger.info "Received expected fail message - #{msg[:reason]}"
96
+ end
97
+ end
98
+ end
99
+
100
+ # 10 seconds later, we will 'release' this engine, i.e. shut it down
101
+ #engine.after(10) { release_engine(engine, garage) }
102
+ end
103
+
104
+ def release_engine(engine, garage)
105
+ logger.info "Time to release engine #{engine}"
106
+ garage.release engine do |rmsg|
107
+ puts "===> ENGINE RELEASED: #{rmsg}"
108
+ end
109
+ end
110
+
111
+ OmfCommon.eventloop.run do |el|
112
+ OmfCommon.comm.on_connected do |comm|
113
+
114
+ # Create garage proxy
115
+ load File.join(File.dirname(__FILE__), '..', '..', 'omf_rc', 'example', 'garage_controller.rb')
116
+ garage_inst = OmfRc::ResourceFactory.create(:garage, hrn: :garage_1)
117
+
118
+ # Get handle on existing entity
119
+ comm.subscribe('garage_1') do |garage|
120
+
121
+ garage.on_inform_failed do |msg|
122
+ logger.error msg
123
+ end
124
+ # wait until garage topic is ready to receive
125
+ garage.on_subscribed do
126
+ create_engine(garage)
127
+ end
128
+ end
129
+
130
+ el.after(20) { el.stop }
131
+ end
132
+ end
133
+
134
+
135
+ puts "DONE"
136
+
data/example/vm_alt.rb ADDED
@@ -0,0 +1,65 @@
1
+
2
+ # Communication setup
3
+ Comm.init(:xmpp)
4
+
5
+ def create_vm(vm_name, host)
6
+ opts = {
7
+ name: 'my_VM_123',
8
+ ubuntu_opts: { bridge: 'br0' },
9
+ vmbuilder_opts: {
10
+ ip: '10.0.0.240',
11
+ net: '10.0.0.0',
12
+ bcast: '10.255.255.255',
13
+ mask: '255.0.0.0',
14
+ gw: '10.0.0.200',
15
+ dns: '10.0.0.200'
16
+ }
17
+ }
18
+ host.create(:vm, opts) do |msg|
19
+ if msg.success?
20
+ vm = msg.resource
21
+ on_vm_created(vm, host)
22
+ else
23
+ logger.error "Resource creation failed - #{msg[:reason]}"
24
+ end
25
+ end
26
+ end
27
+
28
+ def on_vm_created(vm, host)
29
+ logger.info "Created #{vm}"
30
+ vm.on_inform_status do |msg|
31
+ msg.each_property do |name, value|
32
+ logger.info "#{name} => #{value}"
33
+ end
34
+ if vm.state == :running
35
+ puts "HURRAY, vm '#{vm}' is up and running"
36
+ end
37
+ end
38
+
39
+ vm.after(10) do
40
+ vm.configure(state: :run)
41
+ end
42
+ end
43
+
44
+ OmfCommon.eventloop.run do |el|
45
+ OmfCommon.comm.on_connected do |comm|
46
+ # Get handle on existing entity
47
+ comm.subscribe('host_1') do |host|
48
+
49
+ host.on_inform_failed do |msg|
50
+ logger.error msg
51
+ end
52
+ # wait until host topic is ready to receive
53
+ host.on_subscribed do
54
+ create_vm(host)
55
+ end
56
+ end
57
+
58
+ el.after(20) { el.stop }
59
+ end
60
+ end
61
+
62
+
63
+ puts "DONE"
64
+
65
+
data/lib/omf_common.rb CHANGED
@@ -1,17 +1,238 @@
1
1
  require 'active_support/core_ext'
2
+
2
3
  require 'omf_common/default_logging'
3
4
  require 'omf_common/version'
4
5
  require 'omf_common/measure'
5
6
  require 'omf_common/message'
6
7
  require 'omf_common/comm'
7
8
  require 'omf_common/command'
8
- require 'omf_common/topic'
9
- require 'omf_common/topic_message'
10
9
  require 'omf_common/key'
11
10
  require 'omf_common/core_ext/string'
12
- require 'omf_common/core_ext/object'
11
+ require 'omf_common/eventloop'
13
12
 
14
13
  include OmfCommon::DefaultLogging
15
14
 
16
15
  module OmfCommon
16
+ DEFAULTS = {
17
+ development: {
18
+ eventloop: {
19
+ type: 'em'
20
+ },
21
+ logging: {
22
+ level: 'debug',
23
+
24
+ appenders: {
25
+ stdout: {
26
+ date_pattern: '%H:%M:%S',
27
+ pattern: '%d %5l %c{2}: %m\n',
28
+ color_scheme: 'default'
29
+ }
30
+ }
31
+ }
32
+ },
33
+ production: {
34
+ eventloop: {
35
+ type: :em
36
+ },
37
+ logging: {
38
+ level: 'info',
39
+
40
+ appenders: {
41
+ file: {
42
+ log_dir: '/var/log',
43
+ #log_file: 'foo.log',
44
+ date_pattern: '%F %T %z',
45
+ pattern: '[%d] %-5l %c: %m\n'
46
+ }
47
+ }
48
+
49
+ }
50
+ },
51
+ local: {
52
+ communication: {
53
+ type: :local,
54
+ },
55
+ eventloop: { type: :local},
56
+ logging: {
57
+ level: 'debug',
58
+
59
+ appenders: {
60
+ stdout: {
61
+ date_pattern: '%H:%M:%S',
62
+ pattern: '%d %5l %c{2}: %m\n',
63
+ color_scheme: 'none'
64
+ }
65
+ }
66
+ }
67
+ },
68
+ test_dev: {
69
+ daemonize: {
70
+ dir_mode: :script,
71
+ dir: '/tmp',
72
+ backtrace: true,
73
+ log_dir: '/tmp',
74
+ log_output: true
75
+ },
76
+ eventloop: {
77
+ type: :local
78
+ },
79
+ logging: {
80
+ level: 'debug',
81
+ appenders: {
82
+ file: {
83
+ log_dir: '/tmp',
84
+ #log_file: 'foo.log',
85
+ date_pattern: '%F %T %z',
86
+ pattern: '[%d] %-5l %c: %m\n'
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ #
94
+ # Initialize the OMF runtime.
95
+ # Options are:
96
+ # :communication
97
+ # :type
98
+ # ... specific opts
99
+ # :eventloop
100
+ # :type {:em|:local...}
101
+ #
102
+ # @param [Hash] opts
103
+ #
104
+ def self.init(op_mode, opts = {}, &block)
105
+ if op_mode && defs = DEFAULTS[op_mode.to_sym]
106
+ opts = _rec_merge(defs, opts)
107
+ end
108
+ if dopts = opts.delete(:daemonize)
109
+ dopts[:app_name] ||= "#{File.basename($0, File.extname($0))}_daemon"
110
+ require 'daemons'
111
+ Daemons.run_proc(dopts[:app_name], dopts) do
112
+ init(nil, opts, &block)
113
+ end
114
+ return
115
+ end
116
+
117
+ if lopts = opts[:logging]
118
+ _init_logging(lopts) unless lopts.empty?
119
+ end
120
+ unless copts = opts[:communication]
121
+ raise "Missing :communication description"
122
+ end
123
+ eopts = opts[:eventloop]
124
+
125
+ # Initialise event loop
126
+ Eventloop.init(eopts)
127
+ # start eventloop immediately if we received a run block
128
+ eventloop.run do
129
+ Comm.init(copts)
130
+ block.call(eventloop) if block
131
+ end
132
+ end
133
+
134
+ # Return the communication driver instance
135
+ #
136
+ def self.comm()
137
+ Comm.instance
138
+ end
139
+
140
+ # Return the communication driver instance
141
+ #
142
+ def self.eventloop()
143
+ Eventloop.instance
144
+ end
145
+
146
+ # Load a YAML file and return it as hash.
147
+ #
148
+ # options:
149
+ # :symbolize_keys FLAG: Symbolize keys if set
150
+ # :path:
151
+ # :same - Look in the same directory as '$0'
152
+ # :remove_root ROOT_NAME: Remove the root node. Throw exception if not ROOT_NAME
153
+ # :wait_for_readable SECS: Wait until the yaml file becomes readable. Check every SECS
154
+ #
155
+ def self.load_yaml(file_name, opts = {})
156
+ if path_opt = opts[:path]
157
+ case path_opt
158
+ when :same
159
+ file_name = File.join(File.dirname($0), file_name)
160
+ else
161
+ raise "Unknown value '#{path_opt}' for 'path' option"
162
+ end
163
+ end
164
+ if readable_check = opts[:wait_for_readable]
165
+ while not File.readable?(file_name)
166
+ puts "WAIT #{file_name}"
167
+ sleep readable_check # wait until file shows up
168
+ end
169
+ end
170
+ yh = YAML.load_file(file_name)
171
+ if opts[:symbolize_keys]
172
+ yh = _rec_sym_keys(yh)
173
+ end
174
+ if root = opts[:remove_root]
175
+ if yh.length != 1 && yh.key?(root)
176
+ raise "Expected root '#{root}', but found '#{yh.keys.inspect}"
177
+ end
178
+ yh = yh.delete(root)
179
+ end
180
+ yh
181
+ end
182
+
183
+ # DO NOT CALL DIRECTLY
184
+ #
185
+ def self._init_logging(opts = {})
186
+ logger = Logging.logger.root
187
+ if appenders = opts[:appenders]
188
+ logger.clear_appenders
189
+ appenders.each do |type, topts|
190
+ case type.to_sym
191
+ when :stdout
192
+ $stdout.sync = true
193
+ logger.add_appenders(
194
+ Logging.appenders.stdout('custom',
195
+ :layout => Logging.layouts.pattern(topts)
196
+ ))
197
+
198
+ when :file
199
+ dir_name = topts.delete(:log_dir) || DEF_LOG_DIR
200
+ file_name = topts.delete(:log_file) || "#{File.basename($0, File.extname($0))}.log"
201
+ path = File.join(dir_name, file_name)
202
+ logger.add_appenders(
203
+ Logging.appenders.file(path,
204
+ :layout => Logging.layouts.pattern(topts)
205
+ ))
206
+ else
207
+ raise "Unknown logging appender type '#{type}'"
208
+ end
209
+ end
210
+ end
211
+ if level = opts[:level]
212
+ logger.level = level.to_sym
213
+ end
214
+ end
215
+
216
+ def self._rec_merge(this_hash, other_hash)
217
+ r = {}
218
+ this_hash.merge(other_hash) do |key, oldval, newval|
219
+ r[key] = oldval.is_a?(Hash) ? _rec_merge(oldval, newval) : newval
220
+ end
221
+ end
222
+
223
+ # Recusively Symbolize keys of hash
224
+ #
225
+ def self._rec_sym_keys(hash)
226
+ h = {}
227
+ hash.each do |k, v|
228
+ if v.is_a? Hash
229
+ v = _rec_sym_keys(v)
230
+ elsif v.is_a? Array
231
+ v = v.map {|e| e.is_a?(Hash) ? _rec_sym_keys(e) : e }
232
+ end
233
+ h[k.to_sym] = v
234
+ end
235
+ h
236
+ end
237
+
17
238
  end