rjr 0.18.2 → 0.19.1
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 +4 -4
- data/Rakefile +2 -0
- data/bin/rjr-client +16 -9
- data/bin/rjr-server +2 -1
- data/examples/client.rb +21 -19
- data/examples/server.rb +1 -1
- data/examples/structured_server.rb +1 -0
- data/examples/tcp.rb +1 -0
- data/lib/rjr/common.rb +1 -226
- data/lib/rjr/core_ext.rb +63 -0
- data/lib/rjr/dispatcher.rb +75 -219
- data/lib/rjr/messages.rb +8 -0
- data/lib/rjr/messages/compressed.rb +264 -0
- data/lib/rjr/messages/notification.rb +95 -0
- data/lib/rjr/messages/request.rb +99 -0
- data/lib/rjr/messages/response.rb +128 -0
- data/lib/rjr/node.rb +100 -97
- data/lib/rjr/node_callback.rb +43 -0
- data/lib/rjr/nodes/amqp.rb +12 -11
- data/lib/rjr/nodes/easy.rb +4 -4
- data/lib/rjr/nodes/local.rb +13 -12
- data/lib/rjr/nodes/multi.rb +1 -1
- data/lib/rjr/nodes/tcp.rb +15 -13
- data/lib/rjr/nodes/template.rb +4 -4
- data/lib/rjr/nodes/unix.rb +15 -13
- data/lib/rjr/nodes/web.rb +15 -14
- data/lib/rjr/nodes/ws.rb +12 -11
- data/lib/rjr/request.rb +128 -0
- data/lib/rjr/result.rb +75 -0
- data/lib/rjr/util/args.rb +145 -0
- data/lib/rjr/{em_adapter.rb → util/em_adapter.rb} +0 -0
- data/lib/rjr/util/handles_methods.rb +115 -0
- data/lib/rjr/util/has_messages.rb +50 -0
- data/lib/rjr/{inspect.rb → util/inspect.rb} +1 -1
- data/lib/rjr/util/json_parser.rb +101 -0
- data/lib/rjr/util/logger.rb +128 -0
- data/lib/rjr/{thread_pool.rb → util/thread_pool.rb} +2 -0
- data/lib/rjr/version.rb +1 -1
- data/site/jrw.js +1 -1
- data/specs/args_spec.rb +144 -0
- data/specs/dispatcher_spec.rb +399 -211
- data/specs/em_adapter_spec.rb +31 -18
- data/specs/handles_methods_spec.rb +154 -0
- data/specs/has_messages_spec.rb +54 -0
- data/specs/inspect_spec.rb +1 -1
- data/specs/json_parser_spec.rb +169 -0
- data/specs/messages/notification_spec.rb +59 -0
- data/specs/messages/request_spec.rb +66 -0
- data/specs/messages/response_spec.rb +94 -0
- data/specs/node_callbacks_spec.rb +47 -0
- data/specs/node_spec.rb +465 -56
- data/specs/request_spec.rb +147 -0
- data/specs/result_spec.rb +144 -0
- data/specs/thread_pool_spec.rb +1 -1
- metadata +41 -11
- data/lib/rjr/errors.rb +0 -23
- data/lib/rjr/message.rb +0 -351
- data/lib/rjr/semaphore.rb +0 -58
- data/specs/message_spec.rb +0 -229
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 517f79e571aae798dcc24be8feacd2ef5cbc3606
|
4
|
+
data.tar.gz: 340a496e1f47092a7b01a4b1363bac1db87b9e81
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b5e056c47862523e346cf2a47923146e0b15a3fb856071f20a4fa226d55fdb6046e5ec463c8aea4e356d96d49e707279d5b477549df2537041943aaa04513f7
|
7
|
+
data.tar.gz: 325453d870c227bd35826a3bab5933fc6a62e644f1efbb2d5de3f762e917b24e38908aa15cfad2d7d3aae2d6d2f8af8da57a7bf527f9558570a78412747c5b60
|
data/Rakefile
CHANGED
data/bin/rjr-client
CHANGED
@@ -5,8 +5,8 @@
|
|
5
5
|
# Licensed under the Apache License, Version 2.0
|
6
6
|
|
7
7
|
require 'optparse'
|
8
|
-
require 'rjr/
|
9
|
-
require 'rjr/
|
8
|
+
require 'rjr/core_ext'
|
9
|
+
require 'rjr/util/logger'
|
10
10
|
require 'rjr/nodes/easy'
|
11
11
|
|
12
12
|
RJR::Logger.log_level = ::Logger::DEBUG
|
@@ -27,7 +27,8 @@ config = { :mode => :msg,
|
|
27
27
|
:msg_id => :rand,
|
28
28
|
:block => false,
|
29
29
|
:interval => 0,
|
30
|
-
:disconnect => false
|
30
|
+
:disconnect => false,
|
31
|
+
:class => 'Object'}
|
31
32
|
|
32
33
|
optparse = OptionParser.new do |opts|
|
33
34
|
opts.on('-h', '--help', 'Display this help screen') do
|
@@ -59,6 +60,10 @@ optparse = OptionParser.new do |opts|
|
|
59
60
|
config[:block] = !ret
|
60
61
|
end
|
61
62
|
|
63
|
+
opts.on('-c', '--class [value]', 'Ruby class to extract messages from (default Class)') do |cls|
|
64
|
+
config[:class] = cls
|
65
|
+
end
|
66
|
+
|
62
67
|
opts.on('-n', '--num number_of_messages', 'Number of messages to send to server (may be a number, :rand, or :indefinite)') do |n|
|
63
68
|
config[:num_msg] = case n.to_s.intern
|
64
69
|
when :rand then rand(MAX_MESSAGES)
|
@@ -68,7 +73,7 @@ optparse = OptionParser.new do |opts|
|
|
68
73
|
end
|
69
74
|
|
70
75
|
opts.on('--message ID', 'Message to send to server (rand to select random)') do |mid|
|
71
|
-
config[:msg_id] = mid
|
76
|
+
config[:msg_id] = (mid == 'rand' ? :rand : mid)
|
72
77
|
end
|
73
78
|
|
74
79
|
opts.on('--interval seconds', 'Number of seconds after which to wait between requests (or rand)') do |s|
|
@@ -94,10 +99,13 @@ NODES = {config[:transport] => {:node_id => config[:node_id],
|
|
94
99
|
:keep_alive => true} } # conditionally set keep alive?
|
95
100
|
|
96
101
|
cdir = File.dirname(__FILE__)
|
97
|
-
client_path = File.join(ENV['RJR_LOAD_PATH'] ||
|
102
|
+
client_path = File.join(ENV['RJR_LOAD_PATH'] ||
|
103
|
+
File.join(cdir, '..', 'examples', 'client'))
|
98
104
|
|
99
105
|
##########################################################
|
100
106
|
|
107
|
+
msg_class = config[:class].to_class
|
108
|
+
|
101
109
|
node = RJR::Nodes::Easy.new(NODES)
|
102
110
|
client_path.split(':').each { |cp|
|
103
111
|
node.dispatcher.add_modules(cp)
|
@@ -112,13 +120,14 @@ if config[:disconnect]
|
|
112
120
|
}
|
113
121
|
end
|
114
122
|
|
123
|
+
|
115
124
|
# invoke request(s)
|
116
125
|
0.upto(config[:num_msg]-1) { |i|
|
117
126
|
# TODO implement mode == :rand
|
118
127
|
|
119
128
|
# grab message (or rand message)
|
120
|
-
msg = (config[:msg_id] == :rand ?
|
121
|
-
|
129
|
+
msg = (config[:msg_id] == :rand ? msg_class.rand_message(config[:transport]) :
|
130
|
+
msg_class.message(config[:msg_id]))
|
122
131
|
|
123
132
|
if msg.nil?
|
124
133
|
puts "Invalid message id"
|
@@ -141,8 +150,6 @@ end
|
|
141
150
|
res = node.invoke(config[:dst], msg[:method], *params)
|
142
151
|
|
143
152
|
# verify and output result
|
144
|
-
puts res
|
145
|
-
puts msg[:result]
|
146
153
|
ress = (msg[:result].nil? ? "" : (msg[:result].call(res) ? "passed" : "failed"))
|
147
154
|
RJR::Logger.info "#{msg[:method]} result #{res} #{ress}"
|
148
155
|
|
data/bin/rjr-server
CHANGED
@@ -62,7 +62,8 @@ NODES = {:amqp => {:node_id => config[:node_id], :broker => config[:broker]},
|
|
62
62
|
:tcp => {:node_id => config[:node_id], :host => config[:host], :port => config[:tcp_port]}}
|
63
63
|
|
64
64
|
cdir = File.dirname(__FILE__)
|
65
|
-
server_path = File.join(ENV['RJR_LOAD_PATH'] ||
|
65
|
+
server_path = File.join(ENV['RJR_LOAD_PATH'] ||
|
66
|
+
File.join(cdir, '..', 'examples', 'server'))
|
66
67
|
|
67
68
|
##########################################################
|
68
69
|
|
data/examples/client.rb
CHANGED
@@ -3,30 +3,32 @@
|
|
3
3
|
# Copyright (C) 2013 Mohammed Morsi <mo@morsi.org>
|
4
4
|
# Licensed under the Apache License, Version 2.0
|
5
5
|
|
6
|
-
|
6
|
+
require 'rjr/util/has_messages'
|
7
7
|
|
8
|
-
|
8
|
+
include RJR::HasMessages
|
9
|
+
|
10
|
+
define_message "stress" do
|
11
|
+
{ :method => 'stress',
|
12
|
+
:params => ["<CLIENT_ID>"],
|
13
|
+
:result => lambda { |r| r =~ /foobar.*/ } }
|
14
|
+
end
|
15
|
+
|
16
|
+
define_message "stress_callback" do
|
17
|
+
{ :method => 'stress_callback',
|
18
|
+
:params => ["<CLIENT_ID>"],
|
19
|
+
:transports => [:tcp, :ws, :amqp],
|
20
|
+
:result => lambda { |r| r =~ /barfoo.*/ } }
|
21
|
+
end
|
22
|
+
|
23
|
+
define_message "messages" do
|
24
|
+
{ :method => 'messages'}
|
25
|
+
end
|
26
|
+
|
27
|
+
def dispatch_examples_client(dispatcher)
|
9
28
|
dispatcher.handle "client_callback" do |p|
|
10
29
|
RJR::Logger.info "invoked client_callback method #{p}"
|
11
30
|
#amqp_node.invoke_request('stress_test-queue', 'stress', "foozmoney#{client_id}")
|
12
31
|
#amqp_node.stop
|
13
32
|
nil
|
14
33
|
end
|
15
|
-
|
16
|
-
define_message "stress" do
|
17
|
-
{ :method => 'stress',
|
18
|
-
:params => ["<CLIENT_ID>"],
|
19
|
-
:result => lambda { |r| r =~ /foobar.*/ } }
|
20
|
-
end
|
21
|
-
|
22
|
-
define_message "stress_callback" do
|
23
|
-
{ :method => 'stress_callback',
|
24
|
-
:params => ["<CLIENT_ID>"],
|
25
|
-
:transports => [:tcp, :ws, :amqp],
|
26
|
-
:result => lambda { |r| r =~ /barfoo.*/ } }
|
27
|
-
end
|
28
|
-
|
29
|
-
define_message "messages" do
|
30
|
-
{ :method => 'messages'}
|
31
|
-
end
|
32
34
|
end
|
data/examples/server.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
# TODO server using HandlesMethods mixin
|
data/examples/tcp.rb
CHANGED
data/lib/rjr/common.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
#
|
5
5
|
# Copyright (C) 2011-2013 Mohammed Morsi <mo@morsi.org>
|
6
6
|
# Licensed under the Apache License, Version 2.0
|
7
|
-
|
7
|
+
|
8
8
|
require 'json'
|
9
9
|
|
10
10
|
# Return a random uuid
|
@@ -25,229 +25,4 @@ def self.persistent_nodes
|
|
25
25
|
}.compact
|
26
26
|
end
|
27
27
|
|
28
|
-
# Logger helper class.
|
29
|
-
#
|
30
|
-
# Encapsulates the standard ruby logger in a thread safe manner. Dispatches
|
31
|
-
# class methods to an internally tracked logger to provide global access.
|
32
|
-
#
|
33
|
-
# TODO handle logging errors (log size too big, logrotate, etc)
|
34
|
-
#
|
35
|
-
# @example
|
36
|
-
# RJR::Logger.info 'my message'
|
37
|
-
# RJR::Logger.warn 'my warning'
|
38
|
-
class Logger
|
39
|
-
private
|
40
|
-
def self._instantiate_logger
|
41
|
-
if @logger.nil?
|
42
|
-
#STDOUT.sync = true
|
43
|
-
output = @log_to || ENV['RJR_LOG'] || STDOUT
|
44
|
-
@logger = ::Logger.new(output)
|
45
|
-
@logger.level = @log_level || ::Logger::FATAL
|
46
|
-
@logger_mutex = Mutex.new
|
47
|
-
@filters = []
|
48
|
-
@highlights = []
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
public
|
53
|
-
|
54
|
-
# Add method which to call on every log message to determine
|
55
|
-
# if messages should be included/excluded
|
56
|
-
def self.add_filter(filter)
|
57
|
-
@logger_mutex.synchronize{
|
58
|
-
@filters << filter
|
59
|
-
}
|
60
|
-
end
|
61
|
-
|
62
|
-
# Add a method which to call on every log message to determine
|
63
|
-
# if message should be highlighted
|
64
|
-
def self.highlight(hlight)
|
65
|
-
@logger_mutex.synchronize{
|
66
|
-
@highlights << hlight
|
67
|
-
}
|
68
|
-
end
|
69
|
-
|
70
|
-
def self.method_missing(method_id, *args)
|
71
|
-
_instantiate_logger
|
72
|
-
@logger_mutex.synchronize {
|
73
|
-
args = args.first if args.first.is_a?(Array)
|
74
|
-
args.each { |a|
|
75
|
-
# run highlights / filters against output before
|
76
|
-
# sending formatted output to logger
|
77
|
-
# TODO allow user to customize highlight mechanism/text
|
78
|
-
na = @highlights.any? { |h| h.call a } ?
|
79
|
-
"\e[1m\e[31m#{a}\e[0m\e[0m" : a
|
80
|
-
@logger.send(method_id, na) if @filters.all? { |f| f.call a }
|
81
|
-
}
|
82
|
-
}
|
83
|
-
end
|
84
|
-
|
85
|
-
def self.safe_exec(*args, &bl)
|
86
|
-
_instantiate_logger
|
87
|
-
@logger_mutex.synchronize {
|
88
|
-
bl.call *args
|
89
|
-
}
|
90
|
-
end
|
91
|
-
|
92
|
-
def self.logger
|
93
|
-
_instantiate_logger
|
94
|
-
@logger
|
95
|
-
end
|
96
|
-
|
97
|
-
# Set log destination
|
98
|
-
# @param dst destination which to log to (file name, STDOUT, etc)
|
99
|
-
def self.log_to(dst)
|
100
|
-
@log_to = dst
|
101
|
-
@logger = nil
|
102
|
-
_instantiate_logger
|
103
|
-
end
|
104
|
-
|
105
|
-
# Set log level.
|
106
|
-
# @param level one of the standard rails log levels (default fatal)
|
107
|
-
def self.log_level=(level)
|
108
|
-
_instantiate_logger
|
109
|
-
if level.is_a?(String)
|
110
|
-
level = case level
|
111
|
-
when 'debug' then
|
112
|
-
::Logger::DEBUG
|
113
|
-
when 'info' then
|
114
|
-
::Logger::INFO
|
115
|
-
when 'warn' then
|
116
|
-
::Logger::WARN
|
117
|
-
when 'error' then
|
118
|
-
::Logger::ERROR
|
119
|
-
when 'fatal' then
|
120
|
-
::Logger::FATAL
|
121
|
-
end
|
122
|
-
end
|
123
|
-
@log_level = level
|
124
|
-
@logger.level = level
|
125
|
-
end
|
126
|
-
|
127
|
-
# Return true if log level is set to debug, else false
|
128
|
-
def self.debug?
|
129
|
-
@log_level == ::Logger::DEBUG
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
end # module RJR
|
134
|
-
|
135
|
-
# Serialized puts, uses logger lock to serialize puts output
|
136
|
-
def sputs(*args)
|
137
|
-
::RJR::Logger.safe_exec {
|
138
|
-
puts *args
|
139
|
-
}
|
140
|
-
end
|
141
|
-
|
142
|
-
class Object
|
143
|
-
def eigenclass
|
144
|
-
class << self
|
145
|
-
self
|
146
|
-
end
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
if RUBY_VERSION < "1.9"
|
151
|
-
# We extend object in ruby 1.9 to define 'instance_exec'
|
152
|
-
#
|
153
|
-
# {http://blog.jayfields.com/2006/09/ruby-instanceexec-aka-instanceeval.html Further reference}
|
154
|
-
class Object
|
155
|
-
module InstanceExecHelper; end
|
156
|
-
include InstanceExecHelper
|
157
|
-
# Execute the specified block in the scope of the local object
|
158
|
-
# @param [Array] args array of args to be passed to block
|
159
|
-
# @param [Callable] block callable object to bind and invoke in the local namespace
|
160
|
-
def instance_exec(*args, &block)
|
161
|
-
begin
|
162
|
-
old_critical, Thread.critical = Thread.critical, true
|
163
|
-
n = 0
|
164
|
-
n += 1 while respond_to?(mname="__instance_exec#{n}")
|
165
|
-
InstanceExecHelper.module_eval{ define_method(mname, &block) }
|
166
|
-
ensure
|
167
|
-
Thread.critical = old_critical
|
168
|
-
end
|
169
|
-
begin
|
170
|
-
ret = send(mname, *args)
|
171
|
-
ensure
|
172
|
-
InstanceExecHelper.module_eval{ remove_method(mname) } rescue nil
|
173
|
-
end
|
174
|
-
ret
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
# Two stage json parsing required, for more details
|
180
|
-
# see json issue https://github.com/flori/json/issues/179
|
181
|
-
|
182
|
-
# FIXME this will only work for json >= 1.7.6 where
|
183
|
-
# create_additions is defined
|
184
|
-
|
185
|
-
class Class
|
186
|
-
class << self
|
187
|
-
attr_accessor :whitelist_json_classes
|
188
|
-
attr_accessor :permitted_json_classes
|
189
|
-
end
|
190
|
-
|
191
|
-
def permit_json_create
|
192
|
-
Class.whitelist_json_classes = true
|
193
|
-
Class.permitted_json_classes ||= []
|
194
|
-
unless Class.permitted_json_classes.include?(self.name)
|
195
|
-
Class.permitted_json_classes << self.name
|
196
|
-
end
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
module RJR
|
201
|
-
def self.invalid_json_class?(jc)
|
202
|
-
Class.whitelist_json_classes ||= false
|
203
|
-
|
204
|
-
Class.whitelist_json_classes ?
|
205
|
-
# only permit classes user explicitly authorizes
|
206
|
-
!Class.permitted_json_classes.include?(jc) :
|
207
|
-
|
208
|
-
# allow any class
|
209
|
-
jc.to_s.split(/::/).inject(Object) do |p,c|
|
210
|
-
case
|
211
|
-
when c.empty? then p
|
212
|
-
when p.constants.collect { |c| c.to_s }.include?(c)
|
213
|
-
then p.const_get(c)
|
214
|
-
else
|
215
|
-
nil
|
216
|
-
end
|
217
|
-
end.nil?
|
218
|
-
end
|
219
|
-
|
220
|
-
def self.validate_json_hash(jh)
|
221
|
-
jh.each { |k,v|
|
222
|
-
if k == ::JSON.create_id && invalid_json_class?(v)
|
223
|
-
raise ArgumentError, "can't create json class #{v}"
|
224
|
-
elsif v.is_a?(Array)
|
225
|
-
validate_json_array(v)
|
226
|
-
elsif v.is_a?(Hash)
|
227
|
-
validate_json_hash(v)
|
228
|
-
end
|
229
|
-
}
|
230
|
-
end
|
231
|
-
|
232
|
-
def self.validate_json_array(ja)
|
233
|
-
ja.each { |jai|
|
234
|
-
if jai.is_a?(Array)
|
235
|
-
validate_json_array(jai)
|
236
|
-
elsif jai.is_a?(Hash)
|
237
|
-
validate_json_hash(jai)
|
238
|
-
end
|
239
|
-
}
|
240
|
-
end
|
241
|
-
|
242
|
-
def self.parse_json(js)
|
243
|
-
jp = ::JSON.parse js, :create_additions => false
|
244
|
-
if jp.is_a?(Array)
|
245
|
-
validate_json_array(jp)
|
246
|
-
elsif jp.is_a?(Hash)
|
247
|
-
validate_json_hash(jp)
|
248
|
-
else
|
249
|
-
return jp
|
250
|
-
end
|
251
|
-
::JSON.parse js, :create_additions => true
|
252
|
-
end
|
253
28
|
end
|
data/lib/rjr/core_ext.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# RJR Ruby Core Extensions
|
2
|
+
#
|
3
|
+
# Copyright (C) 2011-2014 Mohammed Morsi <mo@morsi.org>
|
4
|
+
# Licensed under the Apache License, Version 2.0
|
5
|
+
|
6
|
+
class String
|
7
|
+
# Safely convert string to ruby class it represents
|
8
|
+
def to_class
|
9
|
+
split(/::/).inject(Object) do |p,c|
|
10
|
+
case
|
11
|
+
when c.empty? then p
|
12
|
+
when p.constants.collect { |c| c.to_s }.include?(c)
|
13
|
+
then p.const_get(c)
|
14
|
+
else
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
if RUBY_VERSION < "1.9"
|
22
|
+
# We extend object in ruby 1.9 to define 'instance_exec'
|
23
|
+
#
|
24
|
+
# {http://blog.jayfields.com/2006/09/ruby-instanceexec-aka-instanceeval.html Further reference}
|
25
|
+
class Object
|
26
|
+
module InstanceExecHelper; end
|
27
|
+
include InstanceExecHelper
|
28
|
+
# Execute the specified block in the scope of the local object
|
29
|
+
# @param [Array] args array of args to be passed to block
|
30
|
+
# @param [Callable] block callable object to bind and invoke in the local namespace
|
31
|
+
def instance_exec(*args, &block)
|
32
|
+
begin
|
33
|
+
old_critical, Thread.critical = Thread.critical, true
|
34
|
+
n = 0
|
35
|
+
n += 1 while respond_to?(mname="__instance_exec#{n}")
|
36
|
+
InstanceExecHelper.module_eval{ define_method(mname, &block) }
|
37
|
+
ensure
|
38
|
+
Thread.critical = old_critical
|
39
|
+
end
|
40
|
+
begin
|
41
|
+
ret = send(mname, *args)
|
42
|
+
ensure
|
43
|
+
InstanceExecHelper.module_eval{ remove_method(mname) } rescue nil
|
44
|
+
end
|
45
|
+
ret
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Class
|
51
|
+
class << self
|
52
|
+
attr_accessor :whitelist_json_classes
|
53
|
+
attr_accessor :permitted_json_classes
|
54
|
+
end
|
55
|
+
|
56
|
+
def permit_json_create
|
57
|
+
Class.whitelist_json_classes = true
|
58
|
+
Class.permitted_json_classes ||= []
|
59
|
+
unless Class.permitted_json_classes.include?(self.name)
|
60
|
+
Class.permitted_json_classes << self.name
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|