rtext 0.5.1 → 0.7.0
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.
- 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|
|