transit-ruby 0.8.552-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yard_redcarpet_ext +1 -0
- data/.yardopts +5 -0
- data/Jarfile +10 -0
- data/LICENSE +202 -0
- data/README.md +173 -0
- data/lib/transit.jar +0 -0
- data/lib/transit.rb +108 -0
- data/lib/transit/date_time_util.rb +39 -0
- data/lib/transit/decoder.rb +115 -0
- data/lib/transit/marshaler/base.rb +179 -0
- data/lib/transit/marshaler/jruby/json.rb +46 -0
- data/lib/transit/marshaler/jruby/messagepack.rb +31 -0
- data/lib/transit/read_handlers.rb +113 -0
- data/lib/transit/reader.rb +65 -0
- data/lib/transit/rolling_cache.rb +70 -0
- data/lib/transit/transit_types.rb +251 -0
- data/lib/transit/write_handlers.rb +438 -0
- data/lib/transit/writer.rb +77 -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 +150 -0
- data/spec/transit/reader_spec.rb +165 -0
- data/spec/transit/rolling_cache_spec.rb +94 -0
- data/spec/transit/round_trip_spec.rb +172 -0
- data/spec/transit/transit_types_spec.rb +136 -0
- data/spec/transit/writer_spec.rb +296 -0
- metadata +168 -0
@@ -0,0 +1,65 @@
|
|
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::Reader converts incoming transit data into appropriate
|
17
|
+
# values/objects in Ruby.
|
18
|
+
# @see https://github.com/cognitect/transit-format
|
19
|
+
class Reader
|
20
|
+
extend Forwardable
|
21
|
+
|
22
|
+
# @!method read
|
23
|
+
# Reads transit values from an IO (file, stream, etc), and
|
24
|
+
# converts each one to the appropriate Ruby object.
|
25
|
+
#
|
26
|
+
# With a block, yields each object to the block as it is processed.
|
27
|
+
#
|
28
|
+
# Without a block, returns a single object.
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# reader = Transit::Reader.new(:json, io)
|
32
|
+
# reader.read {|obj| do_something_with(obj)}
|
33
|
+
#
|
34
|
+
# reader = Transit::Reader.new(:json, io)
|
35
|
+
# obj = reader.read
|
36
|
+
def_delegators :@reader, :read
|
37
|
+
|
38
|
+
# @param [Symbol] format required any of :msgpack, :json, :json_verbose
|
39
|
+
# @param [IO] io required
|
40
|
+
# @param [Hash] opts optional
|
41
|
+
# Creates a new Reader configured to read from <tt>io</tt>,
|
42
|
+
# expecting <tt>format</tt> (<tt>:json</tt>, <tt>:msgpack</tt>).
|
43
|
+
#
|
44
|
+
# Use opts to register custom read handlers, associating each one
|
45
|
+
# with its tag.
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
#
|
49
|
+
# json_reader = Transit::Reader.new(:json, io)
|
50
|
+
# # ^^ reads both :json and :json_verbose formats ^^
|
51
|
+
# msgpack_writer = Transit::Reader.new(:msgpack, io)
|
52
|
+
# writer_with_custom_handlers = Transit::Reader.new(:json, io,
|
53
|
+
# :handlers => {"point" => PointReadHandler})
|
54
|
+
#
|
55
|
+
# @see Transit::ReadHandlers
|
56
|
+
def initialize(format, io, opts={})
|
57
|
+
@reader = case format
|
58
|
+
when :json, :json_verbose
|
59
|
+
Unmarshaler::Json.new(io, opts)
|
60
|
+
else
|
61
|
+
Unmarshaler::MessagePack.new(io, opts)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,70 @@
|
|
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
|
+
# @api private
|
17
|
+
class RollingCache
|
18
|
+
extend Forwardable
|
19
|
+
|
20
|
+
def_delegators "@key_to_value", :has_key?, :size
|
21
|
+
|
22
|
+
FIRST_ORD = 48
|
23
|
+
LAST_ORD = 91
|
24
|
+
CACHE_CODE_DIGITS = 44;
|
25
|
+
CACHE_SIZE = CACHE_CODE_DIGITS * CACHE_CODE_DIGITS;
|
26
|
+
MIN_SIZE_CACHEABLE = 4
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
clear
|
30
|
+
end
|
31
|
+
|
32
|
+
def read(key)
|
33
|
+
@key_to_value[key]
|
34
|
+
end
|
35
|
+
|
36
|
+
def write(val)
|
37
|
+
@value_to_key[val] || begin
|
38
|
+
clear if @key_to_value.size >= CACHE_SIZE
|
39
|
+
key = next_key(@key_to_value.size)
|
40
|
+
@value_to_key[val] = key
|
41
|
+
@key_to_value[key] = val
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def cache_key?(str, _=false)
|
46
|
+
str[0] == SUB && str != MAP_AS_ARRAY
|
47
|
+
end
|
48
|
+
|
49
|
+
def cacheable?(str, as_map_key=false)
|
50
|
+
str.size >= MIN_SIZE_CACHEABLE && (as_map_key || str.start_with?("~#","~$","~:"))
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def clear
|
56
|
+
@key_to_value = {}
|
57
|
+
@value_to_key = {}
|
58
|
+
end
|
59
|
+
|
60
|
+
def next_key(i)
|
61
|
+
hi = i / CACHE_CODE_DIGITS;
|
62
|
+
lo = i % CACHE_CODE_DIGITS;
|
63
|
+
if hi == 0
|
64
|
+
"^#{(lo+FIRST_ORD).chr}"
|
65
|
+
else
|
66
|
+
"^#{(hi+FIRST_ORD).chr}#{(lo+FIRST_ORD).chr}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,251 @@
|
|
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
|
+
class Wrapper
|
17
|
+
extend Forwardable
|
18
|
+
|
19
|
+
def_delegators :@value, :hash, :to_sym, :to_s
|
20
|
+
|
21
|
+
attr_reader :value
|
22
|
+
|
23
|
+
def initialize(value)
|
24
|
+
@value = value
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
other.is_a?(self.class) && @value == other.value
|
29
|
+
end
|
30
|
+
alias eql? ==
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
"<#{self.class} \"#{to_s}\">"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Represents a transit symbol extension type.
|
38
|
+
# @see https://github.com/cognitect/transit-format
|
39
|
+
class Symbol < Wrapper
|
40
|
+
def initialize(sym)
|
41
|
+
super sym.to_sym
|
42
|
+
end
|
43
|
+
|
44
|
+
def namespace
|
45
|
+
@namespace ||= parsed[-2]
|
46
|
+
end
|
47
|
+
|
48
|
+
def name
|
49
|
+
@name ||= parsed[-1] || "/"
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def parsed
|
55
|
+
@parsed ||= @value.to_s.split("/")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Represents a transit byte array extension type.
|
60
|
+
# @see https://github.com/cognitect/transit-format
|
61
|
+
class ByteArray < Wrapper
|
62
|
+
def self.from_base64(data)
|
63
|
+
new(Base64.decode64(data))
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_base64
|
67
|
+
Base64.encode64(@value)
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
@value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Represents a transit UUID extension type.
|
76
|
+
# @see https://github.com/cognitect/transit-format
|
77
|
+
class UUID
|
78
|
+
def self.random
|
79
|
+
new
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(uuid_or_most_significant_bits=nil,least_significant_bits=nil)
|
83
|
+
case uuid_or_most_significant_bits
|
84
|
+
when String
|
85
|
+
@string_rep = uuid_or_most_significant_bits
|
86
|
+
when Array
|
87
|
+
@numeric_rep = uuid_or_most_significant_bits.map {|n| twos_complement(n)}
|
88
|
+
when Numeric
|
89
|
+
@numeric_rep = [twos_complement(uuid_or_most_significant_bits), twos_complement(least_significant_bits)]
|
90
|
+
when nil
|
91
|
+
@string_rep = SecureRandom.uuid
|
92
|
+
else
|
93
|
+
raise "Can't build UUID from #{uuid_or_most_significant_bits.inspect}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_s
|
98
|
+
@string_rep ||= numbers_to_string
|
99
|
+
end
|
100
|
+
|
101
|
+
def most_significant_bits
|
102
|
+
@most_significant_bits ||= numeric_rep[0]
|
103
|
+
end
|
104
|
+
|
105
|
+
def least_significant_bits
|
106
|
+
@least_significant_bits ||= numeric_rep[1]
|
107
|
+
end
|
108
|
+
|
109
|
+
def inspect
|
110
|
+
@inspect ||= "<#{self.class} \"#{to_s}\">"
|
111
|
+
end
|
112
|
+
|
113
|
+
def ==(other)
|
114
|
+
return false unless other.is_a?(self.class)
|
115
|
+
if @numeric_rep
|
116
|
+
other.most_significant_bits == most_significant_bits &&
|
117
|
+
other.least_significant_bits == least_significant_bits
|
118
|
+
else
|
119
|
+
other.to_s == @string_rep
|
120
|
+
end
|
121
|
+
end
|
122
|
+
alias eql? ==
|
123
|
+
|
124
|
+
def hash
|
125
|
+
most_significant_bits.hash + least_significant_bits.hash
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def numeric_rep
|
131
|
+
@numeric_rep ||= string_to_numbers
|
132
|
+
end
|
133
|
+
|
134
|
+
def numbers_to_string
|
135
|
+
most_significant_bits = @numeric_rep[0]
|
136
|
+
least_significant_bits = @numeric_rep[1]
|
137
|
+
digits(most_significant_bits >> 32, 8) + "-" +
|
138
|
+
digits(most_significant_bits >> 16, 4) + "-" +
|
139
|
+
digits(most_significant_bits, 4) + "-" +
|
140
|
+
digits(least_significant_bits >> 48, 4) + "-" +
|
141
|
+
digits(least_significant_bits, 12)
|
142
|
+
end
|
143
|
+
|
144
|
+
def string_to_numbers
|
145
|
+
str = @string_rep.delete("-")
|
146
|
+
[twos_complement(str[ 0..15].hex), twos_complement(str[16..31].hex)]
|
147
|
+
end
|
148
|
+
|
149
|
+
def digits(val, digits)
|
150
|
+
hi = 1 << (digits*4)
|
151
|
+
(hi | (val & (hi - 1))).to_s(16)[1..-1]
|
152
|
+
end
|
153
|
+
|
154
|
+
def twos_complement(integer_value, num_of_bits=64)
|
155
|
+
max_signed = 2**(num_of_bits-1)
|
156
|
+
max_unsigned = 2**num_of_bits
|
157
|
+
(integer_value >= max_signed) ? integer_value - max_unsigned : integer_value
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Represents a transit hypermedia link extension type.
|
162
|
+
# @see https://github.com/cognitect/transit-format
|
163
|
+
# @see http://amundsen.com/media-types/collection/format/#arrays-links
|
164
|
+
class Link
|
165
|
+
KEYS = ["href", "rel", "name", "render", "prompt"]
|
166
|
+
RENDER_VALUES = ["link", "image"]
|
167
|
+
|
168
|
+
# @overload Link.new(hash)
|
169
|
+
# @param [Hash] hash
|
170
|
+
# Valid keys are:
|
171
|
+
# "href" required, String or Addressable::URI
|
172
|
+
# "rel" required, String
|
173
|
+
# "name" optional, String
|
174
|
+
# "render" optional, String (only "link" or "image")
|
175
|
+
# "prompt" optional, String
|
176
|
+
# @overload Link.new(href, rel, name, render, prompt)
|
177
|
+
# @param [String, Addressable::URI] href required
|
178
|
+
# @param [String] rel required
|
179
|
+
# @param [String] name optional
|
180
|
+
# @param [String] render optional (only "link" or "image")
|
181
|
+
# @param [String] prompt optional
|
182
|
+
def initialize(*args)
|
183
|
+
@values = if args[0].is_a?(Hash)
|
184
|
+
reconcile_values(args[0])
|
185
|
+
elsif args.length >= 2 && (args[0].is_a?(Addressable::URI) || args[0].is_a?(String))
|
186
|
+
reconcile_values(Hash[KEYS.zip(args)])
|
187
|
+
else
|
188
|
+
raise ArgumentError, "The first argument to Link.new can be a URI, String or a Hash. When the first argument is a URI or String, the second argument, rel, must present."
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def href; @href ||= @values["href"] end
|
193
|
+
def rel; @rel ||= @values["rel"] end
|
194
|
+
def name; @name ||= @values["name"] end
|
195
|
+
def render; @render ||= @values["render"] end
|
196
|
+
def prompt; @prompt ||= @values["prompt"] end
|
197
|
+
|
198
|
+
def to_h
|
199
|
+
@values
|
200
|
+
end
|
201
|
+
|
202
|
+
def ==(other)
|
203
|
+
other.is_a?(Link) && other.to_h == to_h
|
204
|
+
end
|
205
|
+
alias eql? ==
|
206
|
+
|
207
|
+
def hash
|
208
|
+
@values.hash
|
209
|
+
end
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
def reconcile_values(map)
|
214
|
+
map.dup.tap do |m|
|
215
|
+
m["href"] = Addressable::URI.parse(m["href"]) if m["href"].is_a?(String)
|
216
|
+
if m["render"]
|
217
|
+
render = m["render"].downcase
|
218
|
+
if RENDER_VALUES.include?(render)
|
219
|
+
m["render"] = render
|
220
|
+
else
|
221
|
+
raise ArgumentError, "render must be either #{RENDER_VALUES[0]} or #{RENDER_VALUES[1]}"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end.freeze
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Represents a transit tag and value. Returned by default when a
|
229
|
+
# reader encounters a tag for which there is no registered
|
230
|
+
# handler. Can also be used in a custom write handler to force
|
231
|
+
# representation to use a transit ground type using a rep for which
|
232
|
+
# there is no registered handler (e.g., an iterable for the
|
233
|
+
# representation of an array).
|
234
|
+
# @see https://github.com/cognitect/transit-format
|
235
|
+
class TaggedValue
|
236
|
+
attr_reader :tag, :rep
|
237
|
+
def initialize(tag, rep)
|
238
|
+
@tag = tag
|
239
|
+
@rep = rep
|
240
|
+
end
|
241
|
+
|
242
|
+
def ==(other)
|
243
|
+
other.is_a?(self.class) && other.tag == @tag && other.rep == @rep
|
244
|
+
end
|
245
|
+
alias eql? ==
|
246
|
+
|
247
|
+
def hash
|
248
|
+
@tag.hash + @rep.hash
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,438 @@
|
|
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(f)
|
241
|
+
return "z" if f.nan?
|
242
|
+
case f
|
243
|
+
when Float::INFINITY, -Float::INFINITY
|
244
|
+
"z"
|
245
|
+
else
|
246
|
+
"d"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def rep(f)
|
251
|
+
return "NaN" if f.nan?
|
252
|
+
case f
|
253
|
+
when Float::INFINITY then "INF"
|
254
|
+
when -Float::INFINITY then "-INF"
|
255
|
+
else f
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def string_rep(f) rep(f).to_s end
|
260
|
+
end
|
261
|
+
|
262
|
+
class BigDecimalHandler
|
263
|
+
def tag(_) "f" end
|
264
|
+
def rep(f) f.to_s("f") end
|
265
|
+
def string_rep(f) rep(f) end
|
266
|
+
end
|
267
|
+
|
268
|
+
class RationalHandler
|
269
|
+
def tag(_) "ratio" end
|
270
|
+
def rep(r) [r.numerator, r.denominator] end
|
271
|
+
def string_rep(_) nil end
|
272
|
+
end
|
273
|
+
|
274
|
+
# TimeHandler, DateTimeHandler, and DateHandler all have different
|
275
|
+
# implementations of string_rep. Here is the rationale:
|
276
|
+
#
|
277
|
+
# For all three, want to write out the same format
|
278
|
+
# e.g. 2014-04-18T18:51:29.478Z, and we want the milliseconds to truncate
|
279
|
+
# rather than round, eg 29.4786 seconds should be 29.478, not 29.479.
|
280
|
+
# - "sss is the number of complete milliseconds since the start of the
|
281
|
+
# second as three decimal digits."
|
282
|
+
# - http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
283
|
+
#
|
284
|
+
# Some data points (see benchmarks/encoding_time.rb)
|
285
|
+
# - Time and DateTime each offer iso8601 methods, but strftime is faster.
|
286
|
+
# - DateTime's strftime (and iso8601) round millis
|
287
|
+
# - Time's strftime (and iso8601) truncate millis
|
288
|
+
# - we don't care about truncate v round for dates (which have 000 ms)
|
289
|
+
# - date.to_datetime.strftime(...) is considerably faster than date.to_time.strftime(...)
|
290
|
+
class TimeHandler
|
291
|
+
def tag(_) "m" end
|
292
|
+
def rep(t) DateTimeUtil.to_millis(t) end
|
293
|
+
def string_rep(t) rep(t).to_s end
|
294
|
+
def verbose_handler() VerboseTimeHandler.new end
|
295
|
+
end
|
296
|
+
|
297
|
+
class DateTimeHandler < TimeHandler
|
298
|
+
def verbose_handler() VerboseDateTimeHandler.new end
|
299
|
+
end
|
300
|
+
|
301
|
+
class DateHandler < TimeHandler
|
302
|
+
def verbose_handler() VerboseDateHandler.new end
|
303
|
+
end
|
304
|
+
|
305
|
+
class VerboseTimeHandler
|
306
|
+
def tag(_) "t" end
|
307
|
+
def rep(t)
|
308
|
+
# .getutc because we don't want to modify t
|
309
|
+
t.getutc.strftime(Transit::TIME_FORMAT)
|
310
|
+
end
|
311
|
+
def string_rep(t) rep(t) end
|
312
|
+
end
|
313
|
+
|
314
|
+
class VerboseDateTimeHandler < VerboseTimeHandler
|
315
|
+
def rep(t)
|
316
|
+
# .utc because to_time already creates a new object
|
317
|
+
t.to_time.utc.strftime(Transit::TIME_FORMAT)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
class VerboseDateHandler < VerboseTimeHandler
|
322
|
+
def rep(d)
|
323
|
+
# to_datetime because DateTime's strftime is faster
|
324
|
+
# thank Time's, and millis are 000 so it doesn't matter
|
325
|
+
# if we truncate or round.
|
326
|
+
d.to_datetime.strftime(Transit::TIME_FORMAT)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
class UuidHandler
|
331
|
+
def tag(_) "u" end
|
332
|
+
def rep(u) [u.most_significant_bits, u.least_significant_bits] end
|
333
|
+
def string_rep(u) u.to_s end
|
334
|
+
end
|
335
|
+
|
336
|
+
class LinkHandler
|
337
|
+
def tag(_) "link" end
|
338
|
+
def rep(l) l.to_h end
|
339
|
+
def string_rep(_) nil end
|
340
|
+
end
|
341
|
+
|
342
|
+
class UriHandler
|
343
|
+
def tag(_) "r" end
|
344
|
+
def rep(u) u.to_s end
|
345
|
+
def string_rep(u) rep(u) end
|
346
|
+
end
|
347
|
+
|
348
|
+
class AddressableUriHandler
|
349
|
+
def tag(_) "r" end
|
350
|
+
def rep(u) u.to_s end
|
351
|
+
def string_rep(u) rep(u) end
|
352
|
+
end
|
353
|
+
|
354
|
+
class ByteArrayHandler
|
355
|
+
def tag(_) "b" end
|
356
|
+
if Transit::jruby?
|
357
|
+
def rep(b)
|
358
|
+
b.value.to_java_bytes
|
359
|
+
end
|
360
|
+
else
|
361
|
+
def rep(b)
|
362
|
+
b.to_base64
|
363
|
+
end
|
364
|
+
end
|
365
|
+
def string_rep(b) rep(b) end
|
366
|
+
end
|
367
|
+
|
368
|
+
class TransitSymbolHandler
|
369
|
+
def tag(_) "$" end
|
370
|
+
def rep(s) s.to_s end
|
371
|
+
def string_rep(s) rep(s) end
|
372
|
+
end
|
373
|
+
|
374
|
+
class ArrayHandler
|
375
|
+
def tag(_) "array" end
|
376
|
+
def rep(a) a end
|
377
|
+
def string_rep(_) nil end
|
378
|
+
end
|
379
|
+
|
380
|
+
class MapHandler
|
381
|
+
def handlers=(handlers)
|
382
|
+
@handlers = handlers
|
383
|
+
end
|
384
|
+
|
385
|
+
def stringable_keys?(m)
|
386
|
+
m.keys.all? {|k| (@handlers[k.class].tag(k).length == 1) }
|
387
|
+
end
|
388
|
+
|
389
|
+
def tag(m)
|
390
|
+
stringable_keys?(m) ? "map" : "cmap"
|
391
|
+
end
|
392
|
+
|
393
|
+
def rep(m)
|
394
|
+
stringable_keys?(m) ? m : m.reduce([]) {|a, kv| a.concat(kv)}
|
395
|
+
end
|
396
|
+
|
397
|
+
def string_rep(_) nil end
|
398
|
+
end
|
399
|
+
|
400
|
+
class SetHandler
|
401
|
+
def tag(_) "set" end
|
402
|
+
def rep(s) s.to_a end
|
403
|
+
def string_rep(_) nil end
|
404
|
+
end
|
405
|
+
|
406
|
+
class TaggedValueHandler
|
407
|
+
def tag(tv) tv.tag end
|
408
|
+
def rep(tv) tv.rep end
|
409
|
+
def string_rep(_) nil end
|
410
|
+
end
|
411
|
+
|
412
|
+
DEFAULT_WRITE_HANDLERS = {
|
413
|
+
NilClass => NilHandler.new,
|
414
|
+
::Symbol => KeywordHandler.new,
|
415
|
+
String => StringHandler.new,
|
416
|
+
TrueClass => TrueHandler.new,
|
417
|
+
FalseClass => FalseHandler.new,
|
418
|
+
Fixnum => IntHandler.new,
|
419
|
+
Bignum => IntHandler.new,
|
420
|
+
Float => FloatHandler.new,
|
421
|
+
BigDecimal => BigDecimalHandler.new,
|
422
|
+
Rational => RationalHandler.new,
|
423
|
+
Time => TimeHandler.new,
|
424
|
+
DateTime => DateTimeHandler.new,
|
425
|
+
Date => DateHandler.new,
|
426
|
+
UUID => UuidHandler.new,
|
427
|
+
Link => LinkHandler.new,
|
428
|
+
URI => UriHandler.new,
|
429
|
+
Addressable::URI => AddressableUriHandler.new,
|
430
|
+
ByteArray => ByteArrayHandler.new,
|
431
|
+
Transit::Symbol => TransitSymbolHandler.new,
|
432
|
+
Array => ArrayHandler.new,
|
433
|
+
Hash => MapHandler.new,
|
434
|
+
Set => SetHandler.new,
|
435
|
+
TaggedValue => TaggedValueHandler.new
|
436
|
+
}.freeze
|
437
|
+
end
|
438
|
+
end
|