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

Sign up to get free protection for your applications and to get access to all the features.
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