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.
@@ -0,0 +1,39 @@
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
+ module DateTimeUtil
18
+ def to_millis(v)
19
+ case v
20
+ when DateTime
21
+ t = v.new_offset(0).to_time
22
+ when Date
23
+ t = Time.gm(v.year, v.month, v.day)
24
+ when Time
25
+ t = v.getutc
26
+ else
27
+ raise "Don't know how to get millis from #{t.inspect}"
28
+ end
29
+ (t.to_i * 1000) + (t.usec / 1000)
30
+ end
31
+
32
+ def from_millis(millis)
33
+ t = Time.at(millis / 1000).utc
34
+ DateTime.new(t.year, t.month, t.day, t.hour, t.min, t.sec + (millis % 1000 * 0.001))
35
+ end
36
+
37
+ module_function :to_millis, :from_millis
38
+ end
39
+ end
@@ -0,0 +1,115 @@
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
+ # Converts a transit value to an instance of a type
17
+ # @api private
18
+ class Decoder
19
+ ESC_ESC = "#{ESC}#{ESC}"
20
+ ESC_SUB = "#{ESC}#{SUB}"
21
+ ESC_RES = "#{ESC}#{RES}"
22
+
23
+ IDENTITY = ->(v){v}
24
+
25
+ GROUND_TAGS = %w[_ s ? i d b ' array map]
26
+
27
+ def initialize(options={})
28
+ custom_handlers = options[:handlers] || {}
29
+ custom_handlers.each {|k,v| validate_handler(k,v)}
30
+ @handlers = ReadHandlers::DEFAULT_READ_HANDLERS.merge(custom_handlers)
31
+ @default_handler = options[:default_handler] || ReadHandlers::DEFAULT_READ_HANDLER
32
+ end
33
+
34
+ # @api private
35
+ class Tag < String; end
36
+
37
+ # Decodes a transit value to a corresponding object
38
+ #
39
+ # @param node a transit value to be decoded
40
+ # @param cache
41
+ # @param as_map_key
42
+ # @return decoded object
43
+ def decode(node, cache=RollingCache.new, as_map_key=false)
44
+ case node
45
+ when String
46
+ if cache.has_key?(node)
47
+ cache.read(node)
48
+ else
49
+ parsed = begin
50
+ if !node.start_with?(ESC)
51
+ node
52
+ elsif node.start_with?(TAG)
53
+ Tag.new(node[2..-1])
54
+ elsif handler = @handlers[node[1]]
55
+ handler.from_rep(node[2..-1])
56
+ elsif node.start_with?(ESC_ESC, ESC_SUB, ESC_RES)
57
+ node[1..-1]
58
+ else
59
+ @default_handler.from_rep(node[1], node[2..-1])
60
+ end
61
+ end
62
+ if cache.cacheable?(node, as_map_key)
63
+ cache.write(parsed)
64
+ end
65
+ parsed
66
+ end
67
+ when Array
68
+ return node if node.empty?
69
+ e0 = decode(node.shift, cache, false)
70
+ if e0 == MAP_AS_ARRAY
71
+ decode(Hash[*node], cache)
72
+ elsif Tag === e0
73
+ v = decode(node.shift, cache)
74
+ if handler = @handlers[e0]
75
+ handler.from_rep(v)
76
+ else
77
+ @default_handler.from_rep(e0,v)
78
+ end
79
+ else
80
+ [e0] + node.map {|e| decode(e, cache, as_map_key)}
81
+ end
82
+ when Hash
83
+ if node.size == 1
84
+ k = decode(node.keys.first, cache, true)
85
+ v = decode(node.values.first, cache, false)
86
+ if Tag === k
87
+ if handler = @handlers[k]
88
+ handler.from_rep(v)
89
+ else
90
+ @default_handler.from_rep(k,v)
91
+ end
92
+ else
93
+ {k => v}
94
+ end
95
+ else
96
+ node.keys.each do |k|
97
+ node.store(decode(k, cache, true), decode(node.delete(k), cache))
98
+ end
99
+ node
100
+ end
101
+ else
102
+ node
103
+ end
104
+ end
105
+
106
+ def validate_handler(key, handler)
107
+ raise ArgumentError.new(CAN_NOT_OVERRIDE_GROUND_TYPES_MESSAGE) if GROUND_TAGS.include?(key)
108
+ end
109
+
110
+ CAN_NOT_OVERRIDE_GROUND_TYPES_MESSAGE = <<-MSG
111
+ You can not supply custom read handlers for ground types.
112
+ MSG
113
+
114
+ end
115
+ end
@@ -0,0 +1,98 @@
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
+ # @see Transit::WriteHandlers
17
+ module ReadHandlers
18
+ class Default
19
+ def from_rep(tag,val) TaggedValue.new(tag, val) end
20
+ end
21
+ class NilHandler
22
+ def from_rep(_) nil end
23
+ end
24
+ class KeywordHandler
25
+ def from_rep(v) v.to_sym end
26
+ end
27
+ class BooleanHandler
28
+ def from_rep(v) v == "t" end
29
+ end
30
+ class ByteArrayHandler
31
+ def from_rep(v) ByteArray.from_base64(v) end
32
+ end
33
+ class FloatHandler
34
+ def from_rep(v) Float(v) end
35
+ end
36
+ class IntegerHandler
37
+ def from_rep(v) v.to_i end
38
+ end
39
+ class BigIntegerHandler
40
+ def from_rep(v) v.to_i end
41
+ end
42
+ class BigDecimalHandler
43
+ def from_rep(v) BigDecimal.new(v) end
44
+ end
45
+ class IdentityHandler
46
+ def from_rep(v) v end
47
+ end
48
+ class SymbolHandler
49
+ def from_rep(v) Transit::Symbol.new(v) end
50
+ end
51
+ class TimeStringHandler
52
+ def from_rep(v) DateTime.iso8601(v) end
53
+ end
54
+ class TimeIntHandler
55
+ def from_rep(v) DateTimeUtil.from_millis(v.to_i) end
56
+ end
57
+ class UuidHandler
58
+ def from_rep(v) UUID.new(v) end
59
+ end
60
+ class UriHandler
61
+ def from_rep(v) Addressable::URI.parse(v) end
62
+ end
63
+ class SetHandler
64
+ def from_rep(v) Set.new(v) end
65
+ end
66
+ class LinkHandler
67
+ def from_rep(v) Link.new(v) end
68
+ end
69
+ class CmapHandler
70
+ def from_rep(v) Hash[*v] end
71
+ end
72
+
73
+ DEFAULT_READ_HANDLERS = {
74
+ "_" => NilHandler.new,
75
+ ":" => KeywordHandler.new,
76
+ "?" => BooleanHandler.new,
77
+ "b" => ByteArrayHandler.new,
78
+ "d" => FloatHandler.new,
79
+ "i" => IntegerHandler.new,
80
+ "n" => BigIntegerHandler.new,
81
+ "f" => BigDecimalHandler.new,
82
+ "c" => IdentityHandler.new,
83
+ "$" => SymbolHandler.new,
84
+ "t" => TimeStringHandler.new,
85
+ "m" => TimeIntHandler.new,
86
+ "u" => UuidHandler.new,
87
+ "r" => UriHandler.new,
88
+ "'" => IdentityHandler.new,
89
+ "set" => SetHandler.new,
90
+ "link" => LinkHandler.new,
91
+ "list" => IdentityHandler.new,
92
+ "cmap" => CmapHandler.new
93
+ }.freeze
94
+
95
+ DEFAULT_READ_HANDLER = Default.new
96
+
97
+ end
98
+ end
@@ -0,0 +1,117 @@
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
+ # @api private
21
+ class JsonUnmarshaler
22
+ class ParseHandler
23
+ def each(&block) @yield_v = block end
24
+ def add_value(v) @yield_v[v] if @yield_v end
25
+
26
+ def hash_start() {} end
27
+ def hash_set(h,k,v) h.store(k,v) end
28
+ def array_start() [] end
29
+ def array_append(a,v) a << v end
30
+
31
+ def error(message, line, column)
32
+ raise Exception.new(message, line, column)
33
+ end
34
+ end
35
+
36
+ def initialize(io, opts)
37
+ @io = io
38
+ @decoder = Transit::Decoder.new(opts)
39
+ @parse_handler = ParseHandler.new
40
+ end
41
+
42
+ # @see Reader#read
43
+ def read
44
+ if block_given?
45
+ @parse_handler.each {|v| yield @decoder.decode(v)}
46
+ else
47
+ @parse_handler.each {|v| return @decoder.decode(v)}
48
+ end
49
+ Oj.sc_parse(@parse_handler, @io)
50
+ end
51
+ end
52
+
53
+ # @api private
54
+ class MessagePackUnmarshaler
55
+ def initialize(io, opts)
56
+ @decoder = Transit::Decoder.new(opts)
57
+ @unpacker = MessagePack::Unpacker.new(io)
58
+ end
59
+
60
+ # @see Reader#read
61
+ def read
62
+ if block_given?
63
+ @unpacker.each {|v| yield @decoder.decode(v)}
64
+ else
65
+ @decoder.decode(@unpacker.read)
66
+ end
67
+ end
68
+ end
69
+
70
+ extend Forwardable
71
+
72
+ # @!method read
73
+ # Reads transit values from an IO (file, stream, etc), and
74
+ # converts each one to the appropriate Ruby object.
75
+ #
76
+ # With a block, yields each object to the block as it is processed.
77
+ #
78
+ # Without a block, returns a single object.
79
+ #
80
+ # @example
81
+ # reader = Transit::Reader.new(:json, io)
82
+ # reader.read {|obj| do_something_with(obj)}
83
+ #
84
+ # reader = Transit::Reader.new(:json, io)
85
+ # obj = reader.read
86
+ def_delegators :@reader, :read
87
+
88
+ # @param [Symbol] format required any of :msgpack, :json, :json_verbose
89
+ # @param [IO] io required
90
+ # @param [Hash] opts optional
91
+ # Creates a new Reader configured to read from <tt>io</tt>,
92
+ # expecting <tt>format</tt> (<tt>:json</tt>, <tt>:msgpack</tt>).
93
+ #
94
+ # Use opts to register custom read handlers, associating each one
95
+ # with its tag.
96
+ #
97
+ # @example
98
+ #
99
+ # json_reader = Transit::Reader.new(:json, io)
100
+ # # ^^ reads both :json and :json_verbose formats ^^
101
+ # msgpack_writer = Transit::Reader.new(:msgpack, io)
102
+ # writer_with_custom_handlers = Transit::Reader.new(:json, io,
103
+ # :handlers => {"point" => PointReadHandler})
104
+ #
105
+ # @see Transit::ReadHandlers
106
+ def initialize(format, io, opts={})
107
+ @reader = case format
108
+ when :json, :json_verbose
109
+ require 'oj'
110
+ JsonUnmarshaler.new(io, opts)
111
+ else
112
+ require 'msgpack'
113
+ MessagePackUnmarshaler.new(io, opts)
114
+ end
115
+ end
116
+ end
117
+ 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