xnlogic-transit-ruby 0.8.572-java
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
- data/.yard_redcarpet_ext +1 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +31 -0
- data/Jarfile +10 -0
- data/LICENSE +202 -0
- data/README.md +172 -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 +123 -0
- data/lib/transit/marshaler/base.rb +193 -0
- data/lib/transit/marshaler/jruby/json.rb +38 -0
- data/lib/transit/marshaler/jruby/messagepack.rb +25 -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 +258 -0
- data/lib/transit/write_handlers.rb +438 -0
- data/lib/transit/writer.rb +68 -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/marshaler_spec.rb +30 -0
- data/spec/transit/reader_spec.rb +170 -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 +144 -0
- data/spec/transit/writer_spec.rb +319 -0
- metadata +171 -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,258 @@
|
|
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
|
+
# For human-readable display only. Use value() for programmatic
|
71
|
+
# consumption of the decoded value.
|
72
|
+
#
|
73
|
+
# Forces the platform's default external encoding, which is
|
74
|
+
# potentially lossy, but also guarantees that something will be
|
75
|
+
# printed instead of raising an error when there is no encoding
|
76
|
+
# information provided.
|
77
|
+
def to_s
|
78
|
+
@value.dup.force_encoding(Encoding.default_external)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Represents a transit UUID extension type.
|
83
|
+
# @see https://github.com/cognitect/transit-format
|
84
|
+
class UUID
|
85
|
+
def self.random
|
86
|
+
new
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialize(uuid_or_most_significant_bits=nil,least_significant_bits=nil)
|
90
|
+
case uuid_or_most_significant_bits
|
91
|
+
when String
|
92
|
+
@string_rep = uuid_or_most_significant_bits
|
93
|
+
when Array
|
94
|
+
@numeric_rep = uuid_or_most_significant_bits.map {|n| twos_complement(n)}
|
95
|
+
when Numeric
|
96
|
+
@numeric_rep = [twos_complement(uuid_or_most_significant_bits), twos_complement(least_significant_bits)]
|
97
|
+
when nil
|
98
|
+
@string_rep = SecureRandom.uuid
|
99
|
+
else
|
100
|
+
raise "Can't build UUID from #{uuid_or_most_significant_bits.inspect}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_s
|
105
|
+
@string_rep ||= numbers_to_string
|
106
|
+
end
|
107
|
+
|
108
|
+
def most_significant_bits
|
109
|
+
@most_significant_bits ||= numeric_rep[0]
|
110
|
+
end
|
111
|
+
|
112
|
+
def least_significant_bits
|
113
|
+
@least_significant_bits ||= numeric_rep[1]
|
114
|
+
end
|
115
|
+
|
116
|
+
def inspect
|
117
|
+
@inspect ||= "<#{self.class} \"#{to_s}\">"
|
118
|
+
end
|
119
|
+
|
120
|
+
def ==(other)
|
121
|
+
return false unless other.is_a?(self.class)
|
122
|
+
if @numeric_rep
|
123
|
+
other.most_significant_bits == most_significant_bits &&
|
124
|
+
other.least_significant_bits == least_significant_bits
|
125
|
+
else
|
126
|
+
other.to_s == @string_rep
|
127
|
+
end
|
128
|
+
end
|
129
|
+
alias eql? ==
|
130
|
+
|
131
|
+
def hash
|
132
|
+
most_significant_bits.hash + least_significant_bits.hash
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def numeric_rep
|
138
|
+
@numeric_rep ||= string_to_numbers
|
139
|
+
end
|
140
|
+
|
141
|
+
def numbers_to_string
|
142
|
+
most_significant_bits = @numeric_rep[0]
|
143
|
+
least_significant_bits = @numeric_rep[1]
|
144
|
+
digits(most_significant_bits >> 32, 8) + "-" +
|
145
|
+
digits(most_significant_bits >> 16, 4) + "-" +
|
146
|
+
digits(most_significant_bits, 4) + "-" +
|
147
|
+
digits(least_significant_bits >> 48, 4) + "-" +
|
148
|
+
digits(least_significant_bits, 12)
|
149
|
+
end
|
150
|
+
|
151
|
+
def string_to_numbers
|
152
|
+
str = @string_rep.delete("-")
|
153
|
+
[twos_complement(str[ 0..15].hex), twos_complement(str[16..31].hex)]
|
154
|
+
end
|
155
|
+
|
156
|
+
def digits(val, digits)
|
157
|
+
hi = 1 << (digits*4)
|
158
|
+
(hi | (val & (hi - 1))).to_s(16)[1..-1]
|
159
|
+
end
|
160
|
+
|
161
|
+
def twos_complement(integer_value, num_of_bits=64)
|
162
|
+
max_signed = 2**(num_of_bits-1)
|
163
|
+
max_unsigned = 2**num_of_bits
|
164
|
+
(integer_value >= max_signed) ? integer_value - max_unsigned : integer_value
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Represents a transit hypermedia link extension type.
|
169
|
+
# @see https://github.com/cognitect/transit-format
|
170
|
+
# @see http://amundsen.com/media-types/collection/format/#arrays-links
|
171
|
+
class Link
|
172
|
+
KEYS = ["href", "rel", "name", "render", "prompt"]
|
173
|
+
RENDER_VALUES = ["link", "image"]
|
174
|
+
|
175
|
+
# @overload Link.new(hash)
|
176
|
+
# @param [Hash] hash
|
177
|
+
# Valid keys are:
|
178
|
+
# "href" required, String or Addressable::URI
|
179
|
+
# "rel" required, String
|
180
|
+
# "name" optional, String
|
181
|
+
# "render" optional, String (only "link" or "image")
|
182
|
+
# "prompt" optional, String
|
183
|
+
# @overload Link.new(href, rel, name, render, prompt)
|
184
|
+
# @param [String, Addressable::URI] href required
|
185
|
+
# @param [String] rel required
|
186
|
+
# @param [String] name optional
|
187
|
+
# @param [String] render optional (only "link" or "image")
|
188
|
+
# @param [String] prompt optional
|
189
|
+
def initialize(*args)
|
190
|
+
@values = if args[0].is_a?(Hash)
|
191
|
+
reconcile_values(args[0])
|
192
|
+
elsif args.length >= 2 && (args[0].is_a?(Addressable::URI) || args[0].is_a?(String))
|
193
|
+
reconcile_values(Hash[KEYS.zip(args)])
|
194
|
+
else
|
195
|
+
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."
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def href; @href ||= @values["href"] end
|
200
|
+
def rel; @rel ||= @values["rel"] end
|
201
|
+
def name; @name ||= @values["name"] end
|
202
|
+
def render; @render ||= @values["render"] end
|
203
|
+
def prompt; @prompt ||= @values["prompt"] end
|
204
|
+
|
205
|
+
def to_h
|
206
|
+
@values
|
207
|
+
end
|
208
|
+
|
209
|
+
def ==(other)
|
210
|
+
other.is_a?(Link) && other.to_h == to_h
|
211
|
+
end
|
212
|
+
alias eql? ==
|
213
|
+
|
214
|
+
def hash
|
215
|
+
@values.hash
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def reconcile_values(map)
|
221
|
+
map.dup.tap do |m|
|
222
|
+
m["href"] = Addressable::URI.parse(m["href"]) if m["href"].is_a?(String)
|
223
|
+
if m["render"]
|
224
|
+
render = m["render"].downcase
|
225
|
+
if RENDER_VALUES.include?(render)
|
226
|
+
m["render"] = render
|
227
|
+
else
|
228
|
+
raise ArgumentError, "render must be either #{RENDER_VALUES[0]} or #{RENDER_VALUES[1]}"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end.freeze
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Represents a transit tag and value. Returned by default when a
|
236
|
+
# reader encounters a tag for which there is no registered
|
237
|
+
# handler. Can also be used in a custom write handler to force
|
238
|
+
# representation to use a transit ground type using a rep for which
|
239
|
+
# there is no registered handler (e.g., an iterable for the
|
240
|
+
# representation of an array).
|
241
|
+
# @see https://github.com/cognitect/transit-format
|
242
|
+
class TaggedValue
|
243
|
+
attr_reader :tag, :rep
|
244
|
+
def initialize(tag, rep)
|
245
|
+
@tag = tag
|
246
|
+
@rep = rep
|
247
|
+
end
|
248
|
+
|
249
|
+
def ==(other)
|
250
|
+
other.is_a?(self.class) && other.tag == @tag && other.rep == @rep
|
251
|
+
end
|
252
|
+
alias eql? ==
|
253
|
+
|
254
|
+
def hash
|
255
|
+
@tag.hash + @rep.hash
|
256
|
+
end
|
257
|
+
end
|
258
|
+
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
|