transit-ruby 0.8.467
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 +7 -0
- checksums.yaml.gz.sig +1 -0
- data.tar.gz.sig +1 -0
- data/.yard_redcarpet_ext +1 -0
- data/.yardopts +5 -0
- data/LICENSE +202 -0
- data/README.md +167 -0
- data/lib/transit.rb +86 -0
- data/lib/transit/date_time_util.rb +39 -0
- data/lib/transit/decoder.rb +115 -0
- data/lib/transit/read_handlers.rb +98 -0
- data/lib/transit/reader.rb +117 -0
- data/lib/transit/rolling_cache.rb +70 -0
- data/lib/transit/transit_types.rb +251 -0
- data/lib/transit/write_handlers.rb +406 -0
- data/lib/transit/writer.rb +346 -0
- data/spec/spec_helper.rb +85 -0
- data/spec/transit/date_time_util_spec.rb +65 -0
- data/spec/transit/decoder_spec.rb +60 -0
- data/spec/transit/exemplar_spec.rb +149 -0
- data/spec/transit/reader_spec.rb +129 -0
- data/spec/transit/rolling_cache_spec.rb +94 -0
- data/spec/transit/round_trip_spec.rb +179 -0
- data/spec/transit/transit_types_spec.rb +136 -0
- data/spec/transit/writer_spec.rb +277 -0
- metadata +271 -0
- metadata.gz.sig +1 -0
@@ -0,0 +1,406 @@
|
|
1
|
+
# Copyright 2014 Cognitect. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
module Transit
|
16
|
+
# WriteHandlers convert instances of Ruby types to their
|
17
|
+
# corresponding Transit semantic types, and ReadHandlers read
|
18
|
+
# convert transit values back into instances of Ruby
|
19
|
+
# types. transit-ruby ships with default sets of WriteHandlers for
|
20
|
+
# each of the Ruby types that map naturally to transit types, and
|
21
|
+
# ReadHandlers for each transit type. For the common case, the
|
22
|
+
# built-in handlers will suffice, but you can add your own extension
|
23
|
+
# types and/or override the built-in handlers.
|
24
|
+
#
|
25
|
+
# ## Custom handlers
|
26
|
+
#
|
27
|
+
# For example, Ruby has Date, Time, and DateTime, each with their
|
28
|
+
# own semantics. Transit has an instance type, which does not
|
29
|
+
# differentiate between Date and Time, so transit-ruby writes Dates,
|
30
|
+
# Times, and DateTimes as transit instances, and reads transit
|
31
|
+
# instances as DateTimes. If your application cares that Dates are
|
32
|
+
# different from DateTimes, you could register custom write and read
|
33
|
+
# handlers, overriding the built-in DateHandler and adding a new
|
34
|
+
# DateReadHandler.
|
35
|
+
#
|
36
|
+
# ### Write handlers
|
37
|
+
#
|
38
|
+
# Write handlers are required to expose <tt>tag</tt>, <tt>rep</tt>, and <tt>string_rep</tt> methods:
|
39
|
+
#
|
40
|
+
# ```ruby
|
41
|
+
# class DateWriteHandler
|
42
|
+
# def tag(_) "D" end
|
43
|
+
# def rep(o) o.to_s end
|
44
|
+
# def string_rep(o) o.to_s end
|
45
|
+
# def verbose_handler(_) nil end # optional - see Verbose write handlers, below
|
46
|
+
# end
|
47
|
+
# ```
|
48
|
+
#
|
49
|
+
# <tt>tag</tt> returns the tag used to identify the transit type
|
50
|
+
# (built-in or extension). It accepts the object being written,
|
51
|
+
# which allows the handler to return different tags for different
|
52
|
+
# semantics, e.g. the built-in IntHandler, which returns the tag "i"
|
53
|
+
# for numbers that fit within a 64-bit signed integer and "n" for
|
54
|
+
# anything outside that range.
|
55
|
+
#
|
56
|
+
# <tt>rep</tt> accepts the object being written and returns its wire
|
57
|
+
# representation. This can be a scalar value (identified by a
|
58
|
+
# one-character tag) or a map (Ruby Hash) or an array (identified by
|
59
|
+
# a multi-character tag).
|
60
|
+
#
|
61
|
+
# <tt>string_rep</tt> accepts the object being written and returns a
|
62
|
+
# string representation. Used when the object is a key in a map.
|
63
|
+
#
|
64
|
+
# ### Read handlers
|
65
|
+
#
|
66
|
+
# Read handlers are required to expose a single <tt>from_rep</tt> method:
|
67
|
+
#
|
68
|
+
# ```ruby
|
69
|
+
# class DateReadHandler
|
70
|
+
# def from_rep(rep)
|
71
|
+
# Date.parse(rep)
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
# ```
|
75
|
+
#
|
76
|
+
# <tt>from_rep</tt> accepts the wire representation (without the tag), and
|
77
|
+
# uses it to build an appropriate Ruby object.
|
78
|
+
#
|
79
|
+
# ### Usage
|
80
|
+
#
|
81
|
+
# ```ruby
|
82
|
+
# io = StringIO.new('','w+')
|
83
|
+
# writer = Transit::Writer.new(:json, io, :handlers => {Date => DateWriteHandler.new})
|
84
|
+
# writer.write(Date.new(2014,7,22))
|
85
|
+
# io.string
|
86
|
+
# # => "[\"~#'\",\"~D2014-07-22\"]\n"
|
87
|
+
#
|
88
|
+
# reader = Transit::Reader.new(:json, StringIO.new(io.string), :handlers => {"D" => DateReadHandler.new})
|
89
|
+
# reader.read
|
90
|
+
# # => #<Date: 2014-07-22 ((2456861j,0s,0n),+0s,2299161j)>
|
91
|
+
# ```
|
92
|
+
#
|
93
|
+
# ## Custom types and representations
|
94
|
+
#
|
95
|
+
# Transit supports scalar and structured representations. The Date
|
96
|
+
# example, above, demonstrates a String representation (scalar) of a
|
97
|
+
# Date. This works well because it is a natural representation, but
|
98
|
+
# it might not be a good solution for a more complex type, e.g. a
|
99
|
+
# Point. While you _could_ represent a Point as a String, e.g.
|
100
|
+
# <tt>("x:37,y:42")</tt>, it would be more efficient and arguably
|
101
|
+
# more natural to represent it as an array of Integers:
|
102
|
+
#
|
103
|
+
# ```ruby
|
104
|
+
# require 'ostruct'
|
105
|
+
# Point = Struct.new(:x,:y) do
|
106
|
+
# def to_a; [x,y] end
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# class PointWriteHandler
|
110
|
+
# def tag(_) "point" end
|
111
|
+
# def rep(o) o.to_a end
|
112
|
+
# def string_rep(_) nil end
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# class PointReadHandler
|
116
|
+
# def from_rep(rep)
|
117
|
+
# Point.new(*rep)
|
118
|
+
# end
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# io = StringIO.new('','w+')
|
122
|
+
# writer = Transit::Writer.new(:json_verbose, io, :handlers => {Point => PointWriteHandler.new})
|
123
|
+
# writer.write(Point.new(37,42))
|
124
|
+
# io.string
|
125
|
+
# # => "{\"~#point\":[37,42]}\n"
|
126
|
+
#
|
127
|
+
# reader = Transit::Reader.new(:json, StringIO.new(io.string),
|
128
|
+
# :handlers => {"point" => PointReadHandler.new})
|
129
|
+
# reader.read
|
130
|
+
# # => #<struct Point x=37, y=42>
|
131
|
+
# ```
|
132
|
+
#
|
133
|
+
# Note that Date used a one-character tag, "D", whereas Point uses a
|
134
|
+
# multi-character tag, "point". Transit expects one-character tags
|
135
|
+
# to have scalar representations (string, integer, float, boolean,
|
136
|
+
# etc) and multi-character tags to have structural representations,
|
137
|
+
# i.e. maps (Ruby Hashes) or arrays.
|
138
|
+
#
|
139
|
+
# ## Verbose write handlers
|
140
|
+
#
|
141
|
+
# Write handlers can, optionally, support the JSON-VERBOSE format by
|
142
|
+
# providing a verbose write handler. Transit uses this for instances
|
143
|
+
# (Ruby Dates, Times, DateTimes) to differentiate between the more
|
144
|
+
# efficient format using an int representing milliseconds since 1970
|
145
|
+
# in JSON mode from the more readable format using a String in
|
146
|
+
# JSON-VERBOSE mode.
|
147
|
+
#
|
148
|
+
# ```ruby
|
149
|
+
# inst = DateTime.new(1985,04,12,23,20,50,"0")
|
150
|
+
#
|
151
|
+
# io = StringIO.new('','w+')
|
152
|
+
# writer = Transit::Writer.new(:json, io)
|
153
|
+
# writer.write(inst)
|
154
|
+
# io.string
|
155
|
+
# #=> "[\"~#'\",\"~m482196050000\"]\n"
|
156
|
+
#
|
157
|
+
# io = StringIO.new('','w+')
|
158
|
+
# writer = Transit::Writer.new(:json_verbose, io)
|
159
|
+
# writer.write(inst)
|
160
|
+
# io.string
|
161
|
+
# #=> "{\"~#'\":\"~t1985-04-12T23:20:50.000Z\"}\n"
|
162
|
+
# ```
|
163
|
+
#
|
164
|
+
# When you want a more human-readable format for your own custom
|
165
|
+
# types in JSON-VERBOSE mode, create a second write handler and add
|
166
|
+
# a <tt>verbose_handler</tt> method to the first handler that
|
167
|
+
# returns an instance of the verbose handler:
|
168
|
+
#
|
169
|
+
# ```ruby
|
170
|
+
# Element = Struct.new(:id, :name)
|
171
|
+
#
|
172
|
+
# class ElementWriteHandler
|
173
|
+
# def tag(_) "el" end
|
174
|
+
# def rep(v) v.id end
|
175
|
+
# def string_rep(v) v.name end
|
176
|
+
# def verbose_handler() ElementVerboseWriteHandler.new end
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# class ElementVerboseWriteHandler < ElementWriteHandler
|
180
|
+
# def rep(v) v.name end
|
181
|
+
# end
|
182
|
+
#
|
183
|
+
# write_handlers = {Element => ElementWriteHandler.new}
|
184
|
+
#
|
185
|
+
# e = Element.new(3, "Lithium")
|
186
|
+
#
|
187
|
+
# io = StringIO.new('','w+')
|
188
|
+
# writer = Transit::Writer.new(:json, io, :handlers => write_handlers)
|
189
|
+
# writer.write(e)
|
190
|
+
# io.string
|
191
|
+
# # => "[\"~#el\",3]\n"
|
192
|
+
#
|
193
|
+
# io = StringIO.new('','w+')
|
194
|
+
# writer = Transit::Writer.new(:json_verbose, io, :handlers => write_handlers)
|
195
|
+
# writer.write(e)
|
196
|
+
# io.string
|
197
|
+
# # => "{\"~#el\":\"Lithium\"}\n"
|
198
|
+
# ```
|
199
|
+
#
|
200
|
+
# Note that you register the same handler collection; transit-ruby takes care of
|
201
|
+
# asking for the verbose_handler for the :json_verbose format.
|
202
|
+
module WriteHandlers
|
203
|
+
class NilHandler
|
204
|
+
def tag(_) "_" end
|
205
|
+
def rep(_) nil end
|
206
|
+
def string_rep(n) nil end
|
207
|
+
end
|
208
|
+
|
209
|
+
class KeywordHandler
|
210
|
+
def tag(_) ":" end
|
211
|
+
def rep(s) s.to_s end
|
212
|
+
def string_rep(s) rep(s) end
|
213
|
+
end
|
214
|
+
|
215
|
+
class StringHandler
|
216
|
+
def tag(_) "s" end
|
217
|
+
def rep(s) s end
|
218
|
+
def string_rep(s) s end
|
219
|
+
end
|
220
|
+
|
221
|
+
class TrueHandler
|
222
|
+
def tag(_) "?" end
|
223
|
+
def rep(_) true end
|
224
|
+
def string_rep(_) "t" end
|
225
|
+
end
|
226
|
+
|
227
|
+
class FalseHandler
|
228
|
+
def tag(_) "?" end
|
229
|
+
def rep(_) false end
|
230
|
+
def string_rep(_) "f" end
|
231
|
+
end
|
232
|
+
|
233
|
+
class IntHandler
|
234
|
+
def tag(i) i > MAX_INT || i < MIN_INT ? "n" : "i" end
|
235
|
+
def rep(i) i > MAX_INT || i < MIN_INT ? i.to_s : i end
|
236
|
+
def string_rep(i) i.to_s end
|
237
|
+
end
|
238
|
+
|
239
|
+
class FloatHandler
|
240
|
+
def tag(_) "d" end
|
241
|
+
def rep(f) f end
|
242
|
+
def string_rep(f) f.to_s end
|
243
|
+
end
|
244
|
+
|
245
|
+
class BigDecimalHandler
|
246
|
+
def tag(_) "f" end
|
247
|
+
def rep(f) f.to_s("f") end
|
248
|
+
def string_rep(f) rep(f) end
|
249
|
+
end
|
250
|
+
|
251
|
+
# TimeHandler, DateTimeHandler, and DateHandler all have different
|
252
|
+
# implementations of string_rep. Here is the rationale:
|
253
|
+
#
|
254
|
+
# For all three, want to write out the same format
|
255
|
+
# e.g. 2014-04-18T18:51:29.478Z, and we want the milliseconds to truncate
|
256
|
+
# rather than round, eg 29.4786 seconds should be 29.478, not 29.479.
|
257
|
+
# - "sss is the number of complete milliseconds since the start of the
|
258
|
+
# second as three decimal digits."
|
259
|
+
# - http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
260
|
+
#
|
261
|
+
# Some data points (see benchmarks/encoding_time.rb)
|
262
|
+
# - Time and DateTime each offer iso8601 methods, but strftime is faster.
|
263
|
+
# - DateTime's strftime (and iso8601) round millis
|
264
|
+
# - Time's strftime (and iso8601) truncate millis
|
265
|
+
# - we don't care about truncate v round for dates (which have 000 ms)
|
266
|
+
# - date.to_datetime.strftime(...) is considerably faster than date.to_time.strftime(...)
|
267
|
+
class TimeHandler
|
268
|
+
def tag(_) "m" end
|
269
|
+
def rep(t) DateTimeUtil.to_millis(t) end
|
270
|
+
def string_rep(t) rep(t).to_s end
|
271
|
+
def verbose_handler() VerboseTimeHandler.new end
|
272
|
+
end
|
273
|
+
|
274
|
+
class DateTimeHandler < TimeHandler
|
275
|
+
def verbose_handler() VerboseDateTimeHandler.new end
|
276
|
+
end
|
277
|
+
|
278
|
+
class DateHandler < TimeHandler
|
279
|
+
def verbose_handler() VerboseDateHandler.new end
|
280
|
+
end
|
281
|
+
|
282
|
+
class VerboseTimeHandler
|
283
|
+
def tag(_) "t" end
|
284
|
+
def rep(t)
|
285
|
+
# .getutc because we don't want to modify t
|
286
|
+
t.getutc.strftime(Transit::TIME_FORMAT)
|
287
|
+
end
|
288
|
+
def string_rep(t) rep(t) end
|
289
|
+
end
|
290
|
+
|
291
|
+
class VerboseDateTimeHandler < VerboseTimeHandler
|
292
|
+
def rep(t)
|
293
|
+
# .utc because to_time already creates a new object
|
294
|
+
t.to_time.utc.strftime(Transit::TIME_FORMAT)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
class VerboseDateHandler < VerboseTimeHandler
|
299
|
+
def rep(d)
|
300
|
+
# to_datetime because DateTime's strftime is faster
|
301
|
+
# thank Time's, and millis are 000 so it doesn't matter
|
302
|
+
# if we truncate or round.
|
303
|
+
d.to_datetime.strftime(Transit::TIME_FORMAT)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
class UuidHandler
|
308
|
+
def tag(_) "u" end
|
309
|
+
def rep(u) [u.most_significant_bits, u.least_significant_bits] end
|
310
|
+
def string_rep(u) u.to_s end
|
311
|
+
end
|
312
|
+
|
313
|
+
class LinkHandler
|
314
|
+
def tag(_) "link" end
|
315
|
+
def rep(l) l.to_h end
|
316
|
+
def string_rep(_) nil end
|
317
|
+
end
|
318
|
+
|
319
|
+
class UriHandler
|
320
|
+
def tag(_) "r" end
|
321
|
+
def rep(u) u.to_s end
|
322
|
+
def string_rep(u) rep(u) end
|
323
|
+
end
|
324
|
+
|
325
|
+
class AddressableUriHandler
|
326
|
+
def tag(_) "r" end
|
327
|
+
def rep(u) u.to_s end
|
328
|
+
def string_rep(u) rep(u) end
|
329
|
+
end
|
330
|
+
|
331
|
+
class ByteArrayHandler
|
332
|
+
def tag(_) "b" end
|
333
|
+
def rep(b) b.to_base64 end
|
334
|
+
def string_rep(b) rep(b) end
|
335
|
+
end
|
336
|
+
|
337
|
+
class TransitSymbolHandler
|
338
|
+
def tag(_) "$" end
|
339
|
+
def rep(s) s.to_s end
|
340
|
+
def string_rep(s) rep(s) end
|
341
|
+
end
|
342
|
+
|
343
|
+
class ArrayHandler
|
344
|
+
def tag(_) "array" end
|
345
|
+
def rep(a) a end
|
346
|
+
def string_rep(_) nil end
|
347
|
+
end
|
348
|
+
|
349
|
+
class MapHandler
|
350
|
+
def handlers=(handlers)
|
351
|
+
@handlers = handlers
|
352
|
+
end
|
353
|
+
|
354
|
+
def stringable_keys?(m)
|
355
|
+
m.keys.all? {|k| (@handlers[k.class].tag(k).length == 1) }
|
356
|
+
end
|
357
|
+
|
358
|
+
def tag(m)
|
359
|
+
stringable_keys?(m) ? "map" : "cmap"
|
360
|
+
end
|
361
|
+
|
362
|
+
def rep(m)
|
363
|
+
stringable_keys?(m) ? m : m.reduce([]) {|a, kv| a.concat(kv)}
|
364
|
+
end
|
365
|
+
|
366
|
+
def string_rep(_) nil end
|
367
|
+
end
|
368
|
+
|
369
|
+
class SetHandler
|
370
|
+
def tag(_) "set" end
|
371
|
+
def rep(s) s.to_a end
|
372
|
+
def string_rep(_) nil end
|
373
|
+
end
|
374
|
+
|
375
|
+
class TaggedValueHandler
|
376
|
+
def tag(tv) tv.tag end
|
377
|
+
def rep(tv) tv.rep end
|
378
|
+
def string_rep(_) nil end
|
379
|
+
end
|
380
|
+
|
381
|
+
DEFAULT_WRITE_HANDLERS = {
|
382
|
+
NilClass => NilHandler.new,
|
383
|
+
::Symbol => KeywordHandler.new,
|
384
|
+
String => StringHandler.new,
|
385
|
+
TrueClass => TrueHandler.new,
|
386
|
+
FalseClass => FalseHandler.new,
|
387
|
+
Fixnum => IntHandler.new,
|
388
|
+
Bignum => IntHandler.new,
|
389
|
+
Float => FloatHandler.new,
|
390
|
+
BigDecimal => BigDecimalHandler.new,
|
391
|
+
Time => TimeHandler.new,
|
392
|
+
DateTime => DateTimeHandler.new,
|
393
|
+
Date => DateHandler.new,
|
394
|
+
UUID => UuidHandler.new,
|
395
|
+
Link => LinkHandler.new,
|
396
|
+
URI => UriHandler.new,
|
397
|
+
Addressable::URI => AddressableUriHandler.new,
|
398
|
+
ByteArray => ByteArrayHandler.new,
|
399
|
+
Transit::Symbol => TransitSymbolHandler.new,
|
400
|
+
Array => ArrayHandler.new,
|
401
|
+
Hash => MapHandler.new,
|
402
|
+
Set => SetHandler.new,
|
403
|
+
TaggedValue => TaggedValueHandler.new
|
404
|
+
}.freeze
|
405
|
+
end
|
406
|
+
end
|
@@ -0,0 +1,346 @@
|
|
1
|
+
# Copyright 2014 Cognitect. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
module Transit
|
16
|
+
# Transit::Writer marshals Ruby objects as transit values to an output stream.
|
17
|
+
# @see https://github.com/cognitect/transit-format
|
18
|
+
class Writer
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
class Marshaler
|
22
|
+
def initialize(opts)
|
23
|
+
@cache_enabled = !opts[:verbose]
|
24
|
+
@prefer_strings = opts[:prefer_strings]
|
25
|
+
@max_int = opts[:max_int]
|
26
|
+
@min_int = opts[:min_int]
|
27
|
+
|
28
|
+
handlers = WriteHandlers::DEFAULT_WRITE_HANDLERS.dup
|
29
|
+
handlers = handlers.merge!(opts[:handlers]) if opts[:handlers]
|
30
|
+
@handlers = (opts[:verbose] ? verbose_handlers(handlers) : handlers)
|
31
|
+
@handlers.values.each do |h|
|
32
|
+
if h.respond_to?(:handlers=)
|
33
|
+
h.handlers=(@handlers)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def find_handler(obj)
|
39
|
+
obj.class.ancestors.each do |a|
|
40
|
+
if handler = @handlers[a]
|
41
|
+
return handler
|
42
|
+
end
|
43
|
+
end
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def verbose_handlers(handlers)
|
48
|
+
handlers.each do |k, v|
|
49
|
+
if v.respond_to?(:verbose_handler) && vh = v.verbose_handler
|
50
|
+
handlers.store(k, vh)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
handlers
|
54
|
+
end
|
55
|
+
|
56
|
+
def escape(s)
|
57
|
+
if s.start_with?(SUB,ESC,RES) && s != "#{SUB} "
|
58
|
+
"#{ESC}#{s}"
|
59
|
+
else
|
60
|
+
s
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def emit_nil(as_map_key, cache)
|
65
|
+
as_map_key ? emit_string(ESC, "_", nil, true, cache) : emit_value(nil)
|
66
|
+
end
|
67
|
+
|
68
|
+
def emit_string(prefix, tag, value, as_map_key, cache)
|
69
|
+
encoded = "#{prefix}#{tag}#{value}"
|
70
|
+
if @cache_enabled && cache.cacheable?(encoded, as_map_key)
|
71
|
+
emit_value(cache.write(encoded), as_map_key)
|
72
|
+
else
|
73
|
+
emit_value(encoded, as_map_key)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def emit_boolean(handler, b, as_map_key, cache)
|
78
|
+
as_map_key ? emit_string(ESC, "?", handler.string_rep(b), true, cache) : emit_value(b)
|
79
|
+
end
|
80
|
+
|
81
|
+
def emit_int(tag, i, as_map_key, cache)
|
82
|
+
if as_map_key || i > @max_int || i < @min_int
|
83
|
+
emit_string(ESC, tag, i, as_map_key, cache)
|
84
|
+
else
|
85
|
+
emit_value(i, as_map_key)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def emit_double(d, as_map_key, cache)
|
90
|
+
as_map_key ? emit_string(ESC, "d", d, true, cache) : emit_value(d)
|
91
|
+
end
|
92
|
+
|
93
|
+
def emit_array(a, cache)
|
94
|
+
emit_array_start(a.size)
|
95
|
+
a.each {|e| marshal(e, false, cache)}
|
96
|
+
emit_array_end
|
97
|
+
end
|
98
|
+
|
99
|
+
def emit_map(m, cache)
|
100
|
+
emit_map_start(m.size)
|
101
|
+
m.each do |k,v|
|
102
|
+
marshal(k, true, cache)
|
103
|
+
marshal(v, false, cache)
|
104
|
+
end
|
105
|
+
emit_map_end
|
106
|
+
end
|
107
|
+
|
108
|
+
def emit_tagged_value(tag, rep, cache)
|
109
|
+
emit_array_start(2)
|
110
|
+
emit_string(ESC, "#", tag, false, cache)
|
111
|
+
marshal(rep, false, cache)
|
112
|
+
emit_array_end
|
113
|
+
end
|
114
|
+
|
115
|
+
def emit_encoded(handler, tag, obj, as_map_key, cache)
|
116
|
+
if tag.length == 1
|
117
|
+
rep = handler.rep(obj)
|
118
|
+
if String === rep
|
119
|
+
emit_string(ESC, tag, rep, as_map_key, cache)
|
120
|
+
elsif as_map_key || @prefer_strings
|
121
|
+
if str_rep = handler.string_rep(obj)
|
122
|
+
emit_string(ESC, tag, str_rep, as_map_key, cache)
|
123
|
+
else
|
124
|
+
raise "Cannot be encoded as String: " + {:tag => tag, :rep => rep, :obj => obj}.to_s
|
125
|
+
end
|
126
|
+
else
|
127
|
+
emit_tagged_value(tag, handler.rep(obj), cache)
|
128
|
+
end
|
129
|
+
elsif as_map_key
|
130
|
+
raise "Cannot be used as a map key: " + {:tag => tag, :rep => rep, :obj => obj}.to_s
|
131
|
+
else
|
132
|
+
emit_tagged_value(tag, handler.rep(obj), cache)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def marshal(obj, as_map_key, cache)
|
137
|
+
handler = find_handler(obj)
|
138
|
+
tag = handler.tag(obj)
|
139
|
+
case tag
|
140
|
+
when "_"
|
141
|
+
emit_nil(as_map_key, cache)
|
142
|
+
when "?"
|
143
|
+
emit_boolean(handler, obj, as_map_key, cache)
|
144
|
+
when "s"
|
145
|
+
emit_string(nil, nil, escape(handler.rep(obj)), as_map_key, cache)
|
146
|
+
when "i"
|
147
|
+
emit_int(tag, handler.rep(obj), as_map_key, cache)
|
148
|
+
when "d"
|
149
|
+
emit_double(handler.rep(obj), as_map_key, cache)
|
150
|
+
when "'"
|
151
|
+
emit_tagged_value(tag, handler.rep(obj), cache)
|
152
|
+
when "array"
|
153
|
+
emit_array(handler.rep(obj), cache)
|
154
|
+
when "map"
|
155
|
+
emit_map(handler.rep(obj), cache)
|
156
|
+
else
|
157
|
+
emit_encoded(handler, tag, obj, as_map_key, cache)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def marshal_top(obj, cache=RollingCache.new)
|
162
|
+
if handler = find_handler(obj)
|
163
|
+
if tag = handler.tag(obj)
|
164
|
+
if tag.length == 1
|
165
|
+
marshal(TaggedValue.new(QUOTE, obj), false, cache)
|
166
|
+
else
|
167
|
+
marshal(obj, false, cache)
|
168
|
+
end
|
169
|
+
flush
|
170
|
+
else
|
171
|
+
raise "Handler must provide a non-nil tag: #{handler.inspect}"
|
172
|
+
end
|
173
|
+
else
|
174
|
+
raise "Can not find a Write Handler for #{obj.inspect}."
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# @api private
|
180
|
+
class BaseJsonMarshaler < Marshaler
|
181
|
+
def default_opts
|
182
|
+
{:prefer_strings => true,
|
183
|
+
:max_int => JSON_MAX_INT,
|
184
|
+
:min_int => JSON_MIN_INT}
|
185
|
+
end
|
186
|
+
|
187
|
+
def initialize(io, opts)
|
188
|
+
@oj = Oj::StreamWriter.new(io)
|
189
|
+
super(default_opts.merge(opts))
|
190
|
+
@state = []
|
191
|
+
end
|
192
|
+
|
193
|
+
def emit_array_start(size)
|
194
|
+
@state << :array
|
195
|
+
@oj.push_array
|
196
|
+
end
|
197
|
+
|
198
|
+
def emit_array_end
|
199
|
+
@state.pop
|
200
|
+
@oj.pop
|
201
|
+
end
|
202
|
+
|
203
|
+
def emit_map_start(size)
|
204
|
+
@state << :map
|
205
|
+
@oj.push_object
|
206
|
+
end
|
207
|
+
|
208
|
+
def emit_map_end
|
209
|
+
@state.pop
|
210
|
+
@oj.pop
|
211
|
+
end
|
212
|
+
|
213
|
+
def emit_value(obj, as_map_key=false)
|
214
|
+
if @state.last == :array
|
215
|
+
@oj.push_value(obj)
|
216
|
+
else
|
217
|
+
as_map_key ? @oj.push_key(obj) : @oj.push_value(obj)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def flush
|
222
|
+
# no-op
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# @api private
|
227
|
+
class JsonMarshaler < BaseJsonMarshaler
|
228
|
+
def emit_map(m, cache)
|
229
|
+
emit_array_start(-1)
|
230
|
+
emit_value("^ ", false)
|
231
|
+
m.each do |k,v|
|
232
|
+
marshal(k, true, cache)
|
233
|
+
marshal(v, false, cache)
|
234
|
+
end
|
235
|
+
emit_array_end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# @api private
|
240
|
+
class VerboseJsonMarshaler < BaseJsonMarshaler
|
241
|
+
def emit_string(prefix, tag, value, as_map_key, cache)
|
242
|
+
emit_value("#{prefix}#{tag}#{value}", as_map_key)
|
243
|
+
end
|
244
|
+
|
245
|
+
def emit_tagged_value(tag, rep, cache)
|
246
|
+
emit_map_start(1)
|
247
|
+
emit_string(ESC, "#", tag, true, cache)
|
248
|
+
marshal(rep, false, cache)
|
249
|
+
emit_map_end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# @api private
|
254
|
+
class MessagePackMarshaler < Marshaler
|
255
|
+
def default_opts
|
256
|
+
{:prefer_strings => false,
|
257
|
+
:max_int => MAX_INT,
|
258
|
+
:min_int => MIN_INT}
|
259
|
+
end
|
260
|
+
|
261
|
+
def initialize(io, opts)
|
262
|
+
@io = io
|
263
|
+
@packer = MessagePack::Packer.new(io)
|
264
|
+
super(default_opts.merge(opts))
|
265
|
+
end
|
266
|
+
|
267
|
+
def emit_array_start(size)
|
268
|
+
@packer.write_array_header(size)
|
269
|
+
end
|
270
|
+
|
271
|
+
def emit_array_end
|
272
|
+
# no-op
|
273
|
+
end
|
274
|
+
|
275
|
+
def emit_map_start(size)
|
276
|
+
@packer.write_map_header(size)
|
277
|
+
end
|
278
|
+
|
279
|
+
def emit_map_end
|
280
|
+
# no-op
|
281
|
+
end
|
282
|
+
|
283
|
+
def emit_value(obj, as_map_key=:ignore)
|
284
|
+
@packer.write(obj)
|
285
|
+
end
|
286
|
+
|
287
|
+
def flush
|
288
|
+
@packer.flush
|
289
|
+
@io.flush
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# @param [Symbol] format required :json, :json_verbose, or :msgpack
|
294
|
+
# @param [IO] io required
|
295
|
+
# @param [Hash] opts optional
|
296
|
+
#
|
297
|
+
# Creates a new Writer configured to write to <tt>io</tt> in
|
298
|
+
# <tt>format</tt> (<tt>:json</tt>, <tt>:json_verbose</tt>,
|
299
|
+
# <tt>:msgpack</tt>).
|
300
|
+
#
|
301
|
+
# Use opts to register custom write handlers, associating each one
|
302
|
+
# with its type.
|
303
|
+
#
|
304
|
+
# @example
|
305
|
+
# json_writer = Transit::Writer.new(:json, io)
|
306
|
+
# json_verbose_writer = Transit::Writer.new(:json_verbose, io)
|
307
|
+
# msgpack_writer = Transit::Writer.new(:msgpack, io)
|
308
|
+
# writer_with_custom_handlers = Transit::Writer.new(:json, io,
|
309
|
+
# :handlers => {Point => PointWriteHandler})
|
310
|
+
#
|
311
|
+
# @see Transit::WriteHandlers
|
312
|
+
def initialize(format, io, opts={})
|
313
|
+
@marshaler = case format
|
314
|
+
when :json
|
315
|
+
require 'oj'
|
316
|
+
JsonMarshaler.new(io,
|
317
|
+
{:prefer_strings => true,
|
318
|
+
:verbose => false,
|
319
|
+
:handlers => {}}.merge(opts))
|
320
|
+
when :json_verbose
|
321
|
+
require 'oj'
|
322
|
+
VerboseJsonMarshaler.new(io,
|
323
|
+
{:prefer_strings => true,
|
324
|
+
:verbose => true,
|
325
|
+
:handlers => {}}.merge(opts))
|
326
|
+
else
|
327
|
+
require 'msgpack'
|
328
|
+
MessagePackMarshaler.new(io,
|
329
|
+
{:prefer_strings => false,
|
330
|
+
:verbose => false,
|
331
|
+
:handlers => {}}.merge(opts))
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# Converts a Ruby object to a transit value and writes it to this
|
336
|
+
# Writer's output stream.
|
337
|
+
#
|
338
|
+
# @param obj the value to write
|
339
|
+
# @example
|
340
|
+
# writer = Transit::Writer.new(:json, io)
|
341
|
+
# writer.write(Date.new(2014,7,22))
|
342
|
+
def write(obj)
|
343
|
+
@marshaler.marshal_top(obj)
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|