rtext 0.5.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +29 -0
- data/MIT-LICENSE +1 -1
- data/RText_Protocol +102 -51
- data/Rakefile +1 -1
- data/lib/rtext/context_builder.rb +8 -1
- data/lib/rtext/default_completer.rb +163 -0
- data/lib/rtext/default_loader.rb +28 -20
- data/lib/rtext/default_resolver.rb +42 -0
- data/lib/rtext/default_service_provider.rb +49 -21
- data/lib/rtext/frontend/connector.rb +2 -1
- data/lib/rtext/instantiator.rb +38 -16
- data/lib/rtext/json_interface.rb +31 -0
- data/lib/rtext/language.rb +24 -4
- data/lib/rtext/message_helper.rb +35 -25
- data/lib/rtext/parser.rb +27 -45
- data/lib/rtext/serializer.rb +13 -5
- data/lib/rtext/service.rb +27 -18
- data/lib/rtext/tokenizer.rb +18 -2
- data/test/completer_test.rb +35 -18
- data/test/context_builder_test.rb +7 -7
- data/test/instantiator_test.rb +116 -48
- data/test/integration/backend.out +13 -10
- data/test/integration/frontend.log +7664 -0
- data/test/integration/test.rb +6 -0
- data/test/message_helper_test.rb +4 -4
- data/test/serializer_test.rb +101 -3
- data/test/tokenizer_test.rb +33 -1
- metadata +7 -5
- data/lib/rtext/completer.rb +0 -128
@@ -0,0 +1,42 @@
|
|
1
|
+
module RText
|
2
|
+
|
3
|
+
class DefaultResolver
|
4
|
+
|
5
|
+
def initialize(lang)
|
6
|
+
@lang = lang
|
7
|
+
end
|
8
|
+
|
9
|
+
def resolve_fragment(fragment)
|
10
|
+
@lang.reference_qualifier.call(fragment.unresolved_refs, fragment)
|
11
|
+
fragment.resolve_local(
|
12
|
+
:use_target_type => @lang.per_type_identifier)
|
13
|
+
end
|
14
|
+
|
15
|
+
def resolve_model(model)
|
16
|
+
@lang.reference_qualifier.call(model.unresolved_refs, model)
|
17
|
+
model.resolve(
|
18
|
+
:fragment_provider => proc {|e|
|
19
|
+
fr = @lang.fragment_ref(e)
|
20
|
+
fr && fr.fragment
|
21
|
+
},
|
22
|
+
:use_target_type => @lang.per_type_identifier)
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_targets(uref, model)
|
26
|
+
@lang.reference_qualifier.call([uref], model)
|
27
|
+
identifier = uref.proxy.targetIdentifier
|
28
|
+
targets = model.index[identifier]
|
29
|
+
targets ||= []
|
30
|
+
if @lang.per_type_identifier
|
31
|
+
feature = @lang.feature_by_name(uref.element.class.ecore, uref.feature_name)
|
32
|
+
if feature
|
33
|
+
targets = targets.select{|t| t.is_a?(feature.eType.instanceClass)}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
targets
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
@@ -1,7 +1,16 @@
|
|
1
|
+
require 'rtext/default_completer'
|
2
|
+
require 'rtext/default_resolver'
|
3
|
+
|
1
4
|
module RText
|
2
5
|
|
3
6
|
class DefaultServiceProvider
|
4
7
|
|
8
|
+
# Creates a DefaultServiceProvider. Options:
|
9
|
+
#
|
10
|
+
# :resolver
|
11
|
+
# a reference resolver responding to the methods provided by DefaultResolver
|
12
|
+
# default: DefaultResolver
|
13
|
+
#
|
5
14
|
def initialize(language, fragmented_model, model_loader, options={})
|
6
15
|
@lang = language
|
7
16
|
@model = fragmented_model
|
@@ -11,6 +20,7 @@ class DefaultServiceProvider
|
|
11
20
|
@model.add_fragment_change_listener(proc {|fragment, kind|
|
12
21
|
@element_name_index = nil
|
13
22
|
})
|
23
|
+
@resolver = options[:resolver] || DefaultResolver.new(language)
|
14
24
|
end
|
15
25
|
|
16
26
|
def language
|
@@ -25,17 +35,30 @@ class DefaultServiceProvider
|
|
25
35
|
end
|
26
36
|
end
|
27
37
|
|
38
|
+
def get_completion_options(context)
|
39
|
+
completer = RText::DefaultCompleter.new(@lang)
|
40
|
+
class << completer
|
41
|
+
attr_accessor :service_provider
|
42
|
+
def reference_options(context)
|
43
|
+
service_provider.get_reference_completion_options(context).collect {|o|
|
44
|
+
DefaultCompleter::CompletionOption.new(o.identifier, "<#{o.type}>")}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
completer.service_provider = self
|
48
|
+
completer.complete(context)
|
49
|
+
end
|
50
|
+
|
28
51
|
ReferenceCompletionOption = Struct.new(:identifier, :type)
|
29
|
-
def get_reference_completion_options(
|
52
|
+
def get_reference_completion_options(context)
|
30
53
|
if @model.environment
|
31
|
-
targets = @model.environment.find(:class =>
|
54
|
+
targets = @model.environment.find(:class => context.feature.eType.instanceClass)
|
32
55
|
else
|
33
|
-
clazz =
|
56
|
+
clazz = context.feature.eType.instanceClass
|
34
57
|
targets = @model.index.values.flatten.select{|e| e.is_a?(clazz)}
|
35
58
|
end
|
36
59
|
index = 0
|
37
60
|
targets.collect{|t|
|
38
|
-
ident = @lang.identifier_provider.call(t, context.element,
|
61
|
+
ident = @lang.identifier_provider.call(t, context.element, context.feature, index)
|
39
62
|
index += 1
|
40
63
|
if ident
|
41
64
|
ReferenceCompletionOption.new(ident, t.class.ecore.name)
|
@@ -46,29 +69,34 @@ class DefaultServiceProvider
|
|
46
69
|
end
|
47
70
|
|
48
71
|
ReferenceTarget = Struct.new(:file, :line, :display_name)
|
49
|
-
def
|
72
|
+
def get_link_targets(link_descriptor)
|
73
|
+
if link_descriptor.backward
|
74
|
+
get_backward_reference_targets(link_descriptor.value,
|
75
|
+
link_descriptor.element, link_descriptor.feature, link_descriptor.index)
|
76
|
+
else
|
77
|
+
get_forward_reference_targets(link_descriptor.value,
|
78
|
+
link_descriptor.element, link_descriptor.feature, link_descriptor.index)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_forward_reference_targets(identifier, element, feature, index)
|
50
83
|
result = []
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
62
|
-
targets && targets.each do |t|
|
63
|
-
if @lang.fragment_ref(t)
|
64
|
-
path = File.expand_path(@lang.fragment_ref(t).fragment.location)
|
65
|
-
result << ReferenceTarget.new(path, @lang.line_number(t), "#{identifier} [#{t.class.ecore.name}]")
|
84
|
+
ref_value = element.getGenericAsArray(feature.name)[index]
|
85
|
+
if ref_value.is_a?(RGen::MetamodelBuilder::MMProxy)
|
86
|
+
uref = RGen::Instantiator::ReferenceResolver::UnresolvedReference.new(
|
87
|
+
element, feature.name, ref_value)
|
88
|
+
targets = @resolver.find_targets(uref, @model)
|
89
|
+
targets.each do |t|
|
90
|
+
if @lang.fragment_ref(t)
|
91
|
+
path = File.expand_path(@lang.fragment_ref(t).fragment.location)
|
92
|
+
result << ReferenceTarget.new(path, @lang.line_number(t), "#{identifier} [#{t.class.ecore.name}]")
|
93
|
+
end
|
66
94
|
end
|
67
95
|
end
|
68
96
|
result
|
69
97
|
end
|
70
98
|
|
71
|
-
def
|
99
|
+
def get_backward_reference_targets(identifier, element, feature, index)
|
72
100
|
result = []
|
73
101
|
targets = @model.index[@lang.identifier_provider.call(element, nil, nil, nil)]
|
74
102
|
if targets && @lang.per_type_identifier
|
@@ -160,6 +160,7 @@ def do_work
|
|
160
160
|
@logger.info "connecting to #{port}" if @logger
|
161
161
|
begin
|
162
162
|
@socket = TCPSocket.new("127.0.0.1", port)
|
163
|
+
@socket.setsockopt(:SOCKET, :RCVBUF, 1000000)
|
163
164
|
rescue Errno::ECONNREFUSED
|
164
165
|
cleanup
|
165
166
|
@connection_listener.call(:timeout) if @connection_listener
|
@@ -187,7 +188,7 @@ def do_work
|
|
187
188
|
repeat = false
|
188
189
|
data = nil
|
189
190
|
begin
|
190
|
-
data = @socket.read_nonblock(
|
191
|
+
data = @socket.read_nonblock(1000000)
|
191
192
|
rescue Errno::EWOULDBLOCK
|
192
193
|
rescue IOError, EOFError, Errno::ECONNRESET
|
193
194
|
socket_closed = true
|
data/lib/rtext/instantiator.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
require 'rgen/ecore/ecore_ext'
|
2
2
|
require 'rgen/instantiator/reference_resolver'
|
3
|
+
require 'rtext/tokenizer'
|
3
4
|
require 'rtext/parser'
|
4
5
|
|
5
6
|
module RText
|
6
7
|
|
7
8
|
class Instantiator
|
9
|
+
include RText::Tokenizer
|
8
10
|
|
9
11
|
# A problem found during instantiation
|
10
12
|
# if the file is not known, it will be nil
|
@@ -37,8 +39,9 @@ class Instantiator
|
|
37
39
|
# object which references the fragment being instantiated, will be set on model elements
|
38
40
|
#
|
39
41
|
# :on_progress
|
40
|
-
# a proc which is called
|
41
|
-
#
|
42
|
+
# a proc which is called with a number measuring the progress made since the last call;
|
43
|
+
# progress is measured by the number of command tokens recognized plus the number of
|
44
|
+
# model elements instantiated.
|
42
45
|
#
|
43
46
|
def instantiate(str, options={})
|
44
47
|
@line_numbers = {}
|
@@ -50,10 +53,14 @@ class Instantiator
|
|
50
53
|
@fragment_ref = options[:fragment_ref]
|
51
54
|
@on_progress_proc = options[:on_progress]
|
52
55
|
@context_class_stack = []
|
53
|
-
parser = Parser.new
|
56
|
+
parser = Parser.new
|
54
57
|
@root_elements.clear
|
55
58
|
parser_problems = []
|
56
|
-
|
59
|
+
tokens = tokenize(str, @lang.reference_regexp,
|
60
|
+
:on_command_token => @on_progress_proc && lambda do
|
61
|
+
@on_progress_proc.call(1)
|
62
|
+
end)
|
63
|
+
parser.parse(tokens,
|
57
64
|
:descent_visitor => lambda do |command|
|
58
65
|
clazz = @lang.class_by_command(command.value, @context_class_stack.last)
|
59
66
|
# in case no class is found, nil will be pushed, this will case the next command
|
@@ -69,9 +76,6 @@ class Instantiator
|
|
69
76
|
unassociated_comments(args[3])
|
70
77
|
end
|
71
78
|
end,
|
72
|
-
:on_command_token => @on_progress_proc && lambda do
|
73
|
-
@on_progress_proc.call
|
74
|
-
end,
|
75
79
|
:problems => parser_problems)
|
76
80
|
parser_problems.each do |p|
|
77
81
|
problem(p.message, p.line)
|
@@ -88,7 +92,7 @@ class Instantiator
|
|
88
92
|
|
89
93
|
def create_element(command, arg_list, element_list, comments, annotation, is_root)
|
90
94
|
clazz = @context_class_stack.last
|
91
|
-
@on_progress_proc.call if @on_progress_proc
|
95
|
+
@on_progress_proc.call(1) if @on_progress_proc
|
92
96
|
if !@lang.has_command(command.value)
|
93
97
|
problem("Unknown command '#{command.value}'", command.line)
|
94
98
|
return
|
@@ -158,15 +162,21 @@ class Instantiator
|
|
158
162
|
end
|
159
163
|
if !feature.many &&
|
160
164
|
(element.getGenericAsArray(role).size > 0 || children.size > 1)
|
161
|
-
|
165
|
+
if children.size == 1
|
166
|
+
# other child was created under another role lable with same name
|
167
|
+
problem("Only one child allowed in role '#{role}'", line_number(children[0]))
|
168
|
+
else
|
169
|
+
problem("Only one child allowed in role '#{role}'", line_number(children[1]))
|
170
|
+
end
|
162
171
|
return
|
163
172
|
end
|
164
|
-
expected_type =
|
173
|
+
expected_type = nil
|
165
174
|
children.each do |c|
|
166
|
-
|
167
|
-
problem("Role '#{role}' can not take a #{c.class.ecore.name}, expected #{expected_type.name.join(", ")}", line_number(c))
|
168
|
-
else
|
175
|
+
begin
|
169
176
|
element.setOrAddGeneric(feature.name, c)
|
177
|
+
rescue StandardError
|
178
|
+
expected_type ||= @lang.concrete_types(feature.eType)
|
179
|
+
problem("Role '#{role}' can not take a #{c.class.ecore.name}, expected #{expected_type.name.join(", ")}", line_number(c))
|
170
180
|
end
|
171
181
|
end
|
172
182
|
else
|
@@ -236,7 +246,16 @@ class Instantiator
|
|
236
246
|
end
|
237
247
|
element.setOrAddGeneric(feature.name, proxy)
|
238
248
|
else
|
239
|
-
|
249
|
+
begin
|
250
|
+
element.setOrAddGeneric(feature.name, v.value)
|
251
|
+
rescue StandardError
|
252
|
+
# backward compatibility for RGen versions not supporting BigDecimal
|
253
|
+
if v.value.is_a?(BigDecimal)
|
254
|
+
element.setOrAddGeneric(feature.name, v.value.to_f)
|
255
|
+
else
|
256
|
+
raise
|
257
|
+
end
|
258
|
+
end
|
240
259
|
end
|
241
260
|
end
|
242
261
|
defined_args[name] = true
|
@@ -283,11 +302,14 @@ class Instantiator
|
|
283
302
|
elsif feature.eType.is_a?(RGen::ECore::EEnum)
|
284
303
|
[:identifier, :string]
|
285
304
|
else
|
286
|
-
{ String => [:string, :identifier],
|
305
|
+
expected = { String => [:string, :identifier],
|
287
306
|
Integer => [:integer],
|
288
307
|
Float => [:float],
|
289
|
-
RGen::MetamodelBuilder::DataTypes::Boolean => [:boolean]
|
308
|
+
RGen::MetamodelBuilder::DataTypes::Boolean => [:boolean],
|
309
|
+
Object => [:string, :identifier, :integer, :float, :boolean]
|
290
310
|
}[feature.eType.instanceClass]
|
311
|
+
raise "unsupported EType instance class: #{feature.eType.instanceClass}" unless expected
|
312
|
+
expected
|
291
313
|
end
|
292
314
|
end
|
293
315
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module RText
|
4
|
+
|
5
|
+
# this module provides an abstract JSON interface;
|
6
|
+
# it is a global configuration point for JSON conversion in RText;
|
7
|
+
# use +set_o2j_converter+ and +set_j2o_converter+ to use other json implementations
|
8
|
+
module JsonInterface
|
9
|
+
|
10
|
+
# set the o2j converter, a proc which takes an object and returns json
|
11
|
+
def self.set_o2j_converter(conv)
|
12
|
+
define_method(:object_to_json, conv)
|
13
|
+
end
|
14
|
+
|
15
|
+
# set the j2o converter, a proc which takes json and returns an object
|
16
|
+
def self.set_j2o_converter(conv)
|
17
|
+
define_method(:json_to_object, conv)
|
18
|
+
end
|
19
|
+
|
20
|
+
def object_to_json(obj)
|
21
|
+
JSON(obj)
|
22
|
+
end
|
23
|
+
|
24
|
+
def json_to_object(json)
|
25
|
+
JSON(json)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
data/lib/rtext/language.rb
CHANGED
@@ -29,6 +29,13 @@ class Language
|
|
29
29
|
# the serializer will take care to insert quotes if the data is not a valid identifier
|
30
30
|
# the features must also occur in :feature_provider if :feature_provider is provided
|
31
31
|
# default: no unquoted arguments
|
32
|
+
#
|
33
|
+
# :labeled_containments
|
34
|
+
# a Proc which receives an EClass and should return the names of this EClass's containment
|
35
|
+
# references which are to be serialized with a label.
|
36
|
+
# note that labels will always automatically be used when references can't be uniquely
|
37
|
+
# derived from contained elements
|
38
|
+
# default: no additional labeled containments
|
32
39
|
#
|
33
40
|
# :argument_format_provider
|
34
41
|
# a Proc which receives an EAttribute and should return a format specification string
|
@@ -133,6 +140,7 @@ class Language
|
|
133
140
|
reject{|f| f.derived} }
|
134
141
|
@unlabled_arguments = options[:unlabled_arguments]
|
135
142
|
@unquoted_arguments = options[:unquoted_arguments]
|
143
|
+
@labeled_containments = options[:labeled_containments]
|
136
144
|
@argument_format_provider = options[:argument_format_provider]
|
137
145
|
@root_classes = options[:root_classes] || default_root_classes(root_epackage)
|
138
146
|
command_name_provider = options[:command_name_provider] || proc{|c| c.name}
|
@@ -215,6 +223,11 @@ class Language
|
|
215
223
|
@unquoted_arguments.call(feature.eContainingClass).include?(feature.name)
|
216
224
|
end
|
217
225
|
|
226
|
+
def labeled_containment?(clazz, feature)
|
227
|
+
return false unless @labeled_containments
|
228
|
+
@labeled_containments.call(clazz).include?(feature.name)
|
229
|
+
end
|
230
|
+
|
218
231
|
def argument_format(feature)
|
219
232
|
@argument_format_provider && @argument_format_provider.call(feature)
|
220
233
|
end
|
@@ -225,9 +238,10 @@ class Language
|
|
225
238
|
|
226
239
|
def containments_by_target_type(clazz, type)
|
227
240
|
map = {}
|
228
|
-
clazz.
|
241
|
+
containments(clazz).each do |r|
|
229
242
|
concrete_types(r.eType).each {|t| (map[t] ||= []) << r}
|
230
243
|
end
|
244
|
+
# the following line should be unnecessary with exception of "uniq"
|
231
245
|
([type]+type.eAllSuperTypes).inject([]){|m,t| m + (map[t] || []) }.uniq
|
232
246
|
end
|
233
247
|
|
@@ -244,7 +258,11 @@ class Language
|
|
244
258
|
end
|
245
259
|
|
246
260
|
def fragment_ref(element)
|
247
|
-
|
261
|
+
begin
|
262
|
+
@fragment_ref_attribute && element.send(@fragment_ref_attribute)
|
263
|
+
rescue NoMethodError
|
264
|
+
false
|
265
|
+
end
|
248
266
|
end
|
249
267
|
|
250
268
|
private
|
@@ -260,7 +278,7 @@ class Language
|
|
260
278
|
@has_command[cmd] = true
|
261
279
|
clazz = c.instanceClass
|
262
280
|
@class_by_command[clazz] ||= {}
|
263
|
-
c.
|
281
|
+
containments(c).collect{|r|
|
264
282
|
[r.eType] + r.eType.eAllSubTypes}.flatten.uniq.each do |t|
|
265
283
|
next if t.abstract
|
266
284
|
cmw = command_name_provider.call(t)
|
@@ -287,11 +305,13 @@ class Language
|
|
287
305
|
end
|
288
306
|
|
289
307
|
# caching
|
290
|
-
[ :
|
308
|
+
[ :features,
|
309
|
+
:containments,
|
291
310
|
:non_containments,
|
292
311
|
:unlabled_arguments,
|
293
312
|
:labled_arguments,
|
294
313
|
:unquoted?,
|
314
|
+
:labeled_containment?,
|
295
315
|
:argument_format,
|
296
316
|
:concrete_types,
|
297
317
|
:containments_by_target_type,
|
data/lib/rtext/message_helper.rb
CHANGED
@@ -1,29 +1,18 @@
|
|
1
|
-
require '
|
1
|
+
require 'rtext/json_interface'
|
2
2
|
|
3
3
|
module RText
|
4
4
|
|
5
5
|
module MessageHelper
|
6
|
+
include JsonInterface
|
6
7
|
|
7
8
|
def serialize_message(obj)
|
8
|
-
|
9
|
-
|
10
|
-
bytes = s.bytes.to_a
|
11
|
-
s.clear
|
12
|
-
bytes.each do |b|
|
13
|
-
if b >= 128 || b == 0x25 # %
|
14
|
-
s << "%#{b.to_s(16)}".force_encoding("binary")
|
15
|
-
else
|
16
|
-
s << b.chr("binary")
|
17
|
-
end
|
18
|
-
end
|
19
|
-
# there are no non ascii-7-bit characters left
|
20
|
-
s.force_encoding("utf-8")
|
21
|
-
end
|
22
|
-
json = JSON(obj)
|
9
|
+
escape_all_strings(obj)
|
10
|
+
json = object_to_json(obj)
|
23
11
|
# the JSON method outputs data in UTF-8 encoding
|
24
12
|
# the RText protocol expects message lengths measured in bytes
|
25
13
|
# there shouldn't be any non-ascii-7-bit characters, though, so json.size would also be ok
|
26
|
-
|
14
|
+
json.prepend(json.bytesize.to_s)
|
15
|
+
json
|
27
16
|
end
|
28
17
|
|
29
18
|
def extract_message(data)
|
@@ -40,21 +29,42 @@ def extract_message(data)
|
|
40
29
|
# encode from binary to utf-8 with :undef => :replace turns all non-ascii-7-bit bytes
|
41
30
|
# into the replacement character (\uFFFD)
|
42
31
|
json.encode!("utf-8", :undef => :replace)
|
43
|
-
obj =
|
32
|
+
obj = json_to_object(json)
|
44
33
|
end
|
45
34
|
end
|
46
35
|
if obj
|
47
|
-
|
48
|
-
# change encoding back to binary
|
49
|
-
# there could still be replacement characters (\uFFFD), turn them into "?"
|
50
|
-
s.encode!("binary", :undef => :replace)
|
51
|
-
s.gsub!(/%[0-9a-fA-F][0-9a-fA-F]/){|m| m[1..2].to_i(16).chr("binary")}
|
52
|
-
s.force_encoding("binary")
|
53
|
-
end
|
36
|
+
unescape_all_strings(obj)
|
54
37
|
end
|
55
38
|
obj
|
56
39
|
end
|
57
40
|
|
41
|
+
def escape_all_strings(obj)
|
42
|
+
each_json_object_string(obj) do |s|
|
43
|
+
s.force_encoding("binary")
|
44
|
+
bytes = s.bytes.to_a
|
45
|
+
s.clear
|
46
|
+
bytes.each do |b|
|
47
|
+
if b >= 128 || b == 0x25 # %
|
48
|
+
s << "%#{b.to_s(16)}".force_encoding("binary")
|
49
|
+
else
|
50
|
+
s << b.chr("binary")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
# there are no non ascii-7-bit characters left
|
54
|
+
s.force_encoding("utf-8")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def unescape_all_strings(obj)
|
59
|
+
each_json_object_string(obj) do |s|
|
60
|
+
# change encoding back to binary
|
61
|
+
# there could still be replacement characters (\uFFFD), turn them into "?"
|
62
|
+
s.encode!("binary", :undef => :replace)
|
63
|
+
s.gsub!(/%[0-9a-fA-F][0-9a-fA-F]/){|m| m[1..2].to_i(16).chr("binary")}
|
64
|
+
s.force_encoding("binary")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
58
68
|
def each_json_object_string(object, &block)
|
59
69
|
if object.is_a?(Hash)
|
60
70
|
object.each_pair do |k, v|
|